Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

auto-update.sh

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

auto-update.shBlame137 lines · 3 contributors
f635e2fClaude1#!/usr/bin/env bash
2#
3# Fast auto-deploy for the standalone box. Installed as a ~60s systemd timer by
4# scripts/standalone-deploy.sh. Polls the deploy branch; when new commits land,
5# it pulls, rebuilds, and runs migrations. Push -> live in ~1-2 min, hands-off.
6#
7# Exits immediately (cheap) when there is nothing new, so a tight interval is fine.
3b77289ccantynz-alt8#
9# Rollback (2026-07-21): a bad deploy used to leave the box with the OLD
10# container already destroyed and the NEW one unhealthy -- a failed health
11# check just exited 1 with nothing serving traffic until a human noticed and
12# fixed it by hand. The previous image is now tagged before every build, and
13# a failed health check re-deploys it automatically instead of leaving a
14# broken container live. A SHA that fails is remembered (FAILED_MARKER) so a
15# persistently broken commit doesn't retry-and-fail every 60s forever -- it
16# waits for a newer commit (or a human clearing the marker) instead.
f635e2fClaude17set -euo pipefail
18
19REPO_DIR="/opt/gluecron"
9de2807Claude20BRANCH="main"
f635e2fClaude21COMPOSE="docker compose -f docker-compose.standalone.yml"
3b77289ccantynz-alt22IMAGE="gluecron-gluecron"
23FAILED_MARKER="$REPO_DIR/.last-failed-deploy-sha"
f635e2fClaude24
25cd "$REPO_DIR"
26git fetch origin "$BRANCH" --quiet
27
28local_sha=$(git rev-parse HEAD)
29remote_sha=$(git rev-parse "origin/$BRANCH")
30[ "$local_sha" = "$remote_sha" ] && exit 0
31
3b77289ccantynz-alt32if [ -f "$FAILED_MARKER" ] && [ "$(cat "$FAILED_MARKER")" = "$remote_sha" ]; then
33 # Already tried and rolled back from this exact commit -- don't loop.
34 exit 0
35fi
36
f635e2fClaude37echo "$(date -Is) deploying $local_sha -> $remote_sha"
3b77289ccantynz-alt38
39# Preserve the last-known-good image BEFORE touching anything, so a bad
40# deploy can roll back to exactly what was running a moment ago. `|| true`:
41# the very first deploy on a fresh box has no prior image to tag.
42docker tag "$IMAGE:latest" "$IMAGE:last-good" 2>/dev/null || true
43prev_sha="$local_sha"
44
f635e2fClaude45git reset --hard "origin/$BRANCH" # untracked .env / backups are preserved
46
8102dd4ccantynz-alt47# Build provenance for the container. The image ships without a .git dir, so
48# src/lib/build-info.ts can only report a real SHA if we hand it one via env.
49# Without this the footer renders "unknown · unknown", /api/version reports a
50# SHA that never changes (so the client auto-update banner can never fire),
51# and the PWA service-worker cache key never rotates between deploys.
52# docker-compose.standalone.yml interpolates these at `up` time.
53GIT_SHA="$(git rev-parse HEAD)"
54GIT_BRANCH="$BRANCH"
55BUILD_SHA="$GIT_SHA"
56export GIT_SHA GIT_BRANCH BUILD_SHA
57
30e70f3ccanty labs58# NOTE: `|| true` is deliberate. On this Coolify co-tenant box the compose
59# also defines a `caddy` service that tries to bind host :80/:443, which
60# Coolify's proxy already owns — so `up` reports a non-zero exit for caddy
61# even though the app (gluecron) started fine. Under `set -e` that would abort
62# the deploy BEFORE the coolify reattach + health gate below. We tolerate the
63# partial failure here and instead gate on the app's OWN health further down.
64$COMPOSE up -d --build || echo "$(date -Is) compose up returned non-zero (likely the co-tenant caddy port conflict) — continuing to app health gate"
5bac517ccanty labs65
66# Co-tenant ingress reattach (Coolify boxes only).
67# `docker compose up --build` recreates the gluecron container, which DROPS
68# any network attachment not declared in docker-compose.standalone.yml. On
69# this box the public ingress is Coolify's Traefik ("coolify-proxy"), which
70# reaches the app over the external "coolify" network via the file route
71# /traefik/dynamic/gluecron.yaml in the coolify-proxy container. Without this
72# reattach, every deploy 502s the site until someone reconnects by hand.
30e70f3ccanty labs73# Idempotent: a no-op if already attached; skipped entirely on a dedicated VPS
3b77289ccantynz-alt74# that has no "coolify" network. Traefik's file provider watches with fsnotify
75# (--providers.file.watch=true), so it notices the container is reachable
76# again within a second or two of the reconnect — no manual nudge needed.
5bac517ccanty labs77if docker network inspect coolify >/dev/null 2>&1; then
78 docker network connect coolify gluecron-gluecron-1 2>/dev/null \
79 && echo "$(date -Is) reattached gluecron to coolify network" \
80 || echo "$(date -Is) coolify network already attached (ok)"
81fi
82
f635e2fClaude83$COMPOSE exec -T gluecron bun run db:migrate || true
84docker image prune -f >/dev/null 2>&1 || true
85
30e70f3ccanty labs86# App health gate — the real success signal (not caddy). Poll the container's
3b77289ccantynz-alt87# own /healthz; if the app itself never comes up, roll back instead of
88# leaving a broken deploy live.
30e70f3ccanty labs89healthy=0
90for _ in $(seq 1 20); do
91 if docker exec gluecron-gluecron-1 wget -qO- --timeout=4 http://localhost:3000/healthz >/dev/null 2>&1; then
92 healthy=1; break
93 fi
94 sleep 3
95done
3b77289ccantynz-alt96
97if [ "$healthy" = "1" ]; then
98 rm -f "$FAILED_MARKER"
99 echo "$(date -Is) deploy complete: $remote_sha (app healthy)"
100 exit 0
101fi
102
103echo "$(date -Is) DEPLOY FAILED: app /healthz never came up for $remote_sha" >&2
104docker logs --tail 80 gluecron-gluecron-1 >&2 || true
105
106if ! docker image inspect "$IMAGE:last-good" >/dev/null 2>&1; then
107 echo "$(date -Is) no last-good image to roll back to (first deploy on this box?) — human intervention required" >&2
108 echo "$remote_sha" > "$FAILED_MARKER"
109 exit 1
110fi
111
112echo "$(date -Is) rolling back to $prev_sha" >&2
113git reset --hard "$prev_sha"
114docker tag "$IMAGE:last-good" "$IMAGE:latest"
115$COMPOSE up -d --no-build || echo "$(date -Is) compose up (rollback) returned non-zero (likely the co-tenant caddy port conflict) — continuing to health gate"
116
117if docker network inspect coolify >/dev/null 2>&1; then
118 docker network connect coolify gluecron-gluecron-1 2>/dev/null || true
119fi
120
121rb_healthy=0
122for _ in $(seq 1 20); do
123 if docker exec gluecron-gluecron-1 wget -qO- --timeout=4 http://localhost:3000/healthz >/dev/null 2>&1; then
124 rb_healthy=1; break
125 fi
126 sleep 3
127done
128
129echo "$remote_sha" > "$FAILED_MARKER"
130
131if [ "$rb_healthy" = "1" ]; then
132 echo "$(date -Is) rollback to $prev_sha succeeded — app healthy again. $remote_sha will not be retried automatically; push a fix or clear $FAILED_MARKER to try again." >&2
30e70f3ccanty labs133 exit 1
134fi
135
3b77289ccantynz-alt136echo "$(date -Is) ROLLBACK ALSO FAILED — human intervention required NOW (site may be down)" >&2
137exit 1