Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

self-deploy.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.

self-deploy.shBlame295 lines · 2 contributors
f2c00b4CC LABS App1#!/usr/bin/env bash
2# =============================================================================
3# BLOCK W — Gluecron self-deploy.
4#
5# Fired by:
6# - src/hooks/post-receive.ts when SELF_HOST_REPO matches the pushed repo
7# AND the ref is refs/heads/main
8# - the bare repo's hooks/post-receive (for SSH receive-pack)
9# - the optional .gluecron/workflows/deploy.yml on the workflow runner
10#
11# Contract:
12# - Detaches into the background via systemd-run (or nohup fallback) so
13# the caller's git push returns in ~1 second
14# - Logs every step to /var/log/gluecron-self-deploy.log
15# - Notifies /api/events/deploy/{started,step,finished} so /admin/deploys
16# streams the live timeline (same wire as the GitHub Actions workflow)
17# - Rolls back via git reflog if the post-deploy smoke fails
18#
19# Operator invariants:
20# - /etc/gluecron.env is the source of env truth (DATABASE_URL,
21# DEPLOY_EVENT_TOKEN, APP_BASE_URL, ANTHROPIC_API_KEY, …)
22# - /opt/gluecron is the working tree on the box, with git remote `origin`
23# pointing at https://gluecron.com/<owner>/<repo>.git (NOT GitHub)
24# - /opt/gluecron/.next/gluecron-server is the compiled Bun binary
25# - systemd unit `gluecron` is Type=notify and ExecStart=$EXEC_START
26#
27# TODO(ops): configure /etc/logrotate.d/gluecron-self-deploy for the log.
28# =============================================================================
29
30set -euo pipefail
31
32WORKING_DIR="${GLUECRON_WORKING_DIR:-/opt/gluecron}"
33LOG="${GLUECRON_SELF_DEPLOY_LOG:-/var/log/gluecron-self-deploy.log}"
34ENV_FILE="${GLUECRON_ENV_FILE:-/etc/gluecron.env}"
35BUN="${GLUECRON_BUN:-/root/.bun/bin/bun}"
36HEALTHZ_URL="${GLUECRON_HEALTHZ_URL:-http://localhost:3010/healthz}"
37PORT="${GLUECRON_PORT:-3010}"
38DETACHED_FLAG="${1:-}"
39
40# ── helpers ────────────────────────────────────────────────────────────────
41ts() { date +'%Y-%m-%dT%H:%M:%S%z'; }
42log() { echo "[$(ts)] $*" | tee -a "$LOG" >&2; }
43
44notify_step() {
45 local NAME="$1" STATUS="$2" DUR="${3:-}"
46 if [ -z "${DEPLOY_EVENT_TOKEN:-}" ] || [ -z "${APP_BASE_URL:-}" ]; then
47 return 0
48 fi
49 local DUR_FIELD=""
50 if [ -n "$DUR" ]; then DUR_FIELD=",\"duration_ms\":$DUR"; fi
51 curl --silent --show-error --max-time 5 \
52 -X POST "$APP_BASE_URL/api/events/deploy/step" \
53 -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
54 -H "content-type: application/json" \
55 --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"step_name\":\"$NAME\",\"status\":\"$STATUS\"$DUR_FIELD}" \
56 >/dev/null 2>&1 || true
57}
58
59# ── re-exec into the background unless already detached ──────────────────
60# When the post-receive hook calls this script, git push is blocked on the
61# child's stdout/stderr. We re-exec ourselves through systemd-run so the
62# original SSH/HTTP receive-pack process can return immediately.
63if [ "$DETACHED_FLAG" != "--inline" ] && [ -z "${GLUECRON_SELF_DEPLOY_DETACHED:-}" ]; then
64 export GLUECRON_SELF_DEPLOY_DETACHED=1
65 if command -v systemd-run >/dev/null 2>&1; then
66 systemd-run --quiet --unit="gluecron-self-deploy-$(date +%s)" \
67 --collect --no-block \
68 bash "$0" --inline "$@" || nohup bash "$0" --inline "$@" >>"$LOG" 2>&1 &
69 else
70 nohup bash "$0" --inline "$@" >>"$LOG" 2>&1 &
71 disown || true
72 fi
73 exit 0
74fi
75
76# Everything below runs in the detached process.
77mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
78touch "$LOG" 2>/dev/null || true
79
80log "==> gluecron self-deploy starting (pid $$)"
81
82# ── 1. Source env ──────────────────────────────────────────────────────────
83if [ -f "$ENV_FILE" ]; then
84 set -a
85 # shellcheck disable=SC1090
86 source "$ENV_FILE"
87 set +a
88 log " v sourced $ENV_FILE"
89else
90 log " ! $ENV_FILE not found — relying on inherited env"
91fi
92
93cd "$WORKING_DIR"
94
95# ── 2. Capture pre-deploy SHA for rollback ────────────────────────────────
96PREV_SHA="$(git rev-parse HEAD 2>/dev/null || echo '')"
97log " v previous SHA: $PREV_SHA"
98
99# ── 3. Pull latest main ────────────────────────────────────────────────────
100GP_START=$(date +%s)
101notify_step "git-pull" "in_progress"
102git fetch --prune origin main 2>&1 | tee -a "$LOG"
103git reset --hard origin/main 2>&1 | tee -a "$LOG"
104NEW_SHA="$(git rev-parse HEAD)"
105RUN_ID="self-${NEW_SHA:0:12}-$(date +%s)"
106log " v pulled to $NEW_SHA (run_id=$RUN_ID)"
107notify_step "git-pull" "succeeded" "$(( ( $(date +%s) - GP_START ) * 1000 ))"
108
109# ── 3.5 Notify deploy started (now that we have NEW_SHA + RUN_ID) ─────────
110if [ -n "${DEPLOY_EVENT_TOKEN:-}" ] && [ -n "${APP_BASE_URL:-}" ]; then
111 curl --silent --show-error --max-time 10 \
112 -X POST "$APP_BASE_URL/api/events/deploy/started" \
113 -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
114 -H "content-type: application/json" \
115 --data "{\"sha\":\"$NEW_SHA\",\"run_id\":\"$RUN_ID\",\"source\":\"self-deploy\"}" \
116 >>"$LOG" 2>&1 || log " ! deploy/started notify failed (non-fatal)"
117fi
118
119START_EPOCH=$(date +%s)
120DEPLOY_FAILED=0
121FAIL_REASON=""
122
123# ── 4. bun install --frozen-lockfile ───────────────────────────────────────
124BI_START=$(date +%s)
125notify_step "bun-install" "in_progress"
126if "$BUN" install --frozen-lockfile >>"$LOG" 2>&1; then
127 log " v bun install ok"
128 notify_step "bun-install" "succeeded" "$(( ( $(date +%s) - BI_START ) * 1000 ))"
129else
130 log " x bun install FAILED"
131 DEPLOY_FAILED=1
132 FAIL_REASON="bun install failed"
133 notify_step "bun-install" "failed" "$(( ( $(date +%s) - BI_START ) * 1000 ))"
134fi
135
136# ── 5. DB migrations (fail loud) ───────────────────────────────────────────
137if [ "$DEPLOY_FAILED" = "0" ]; then
138 DM_START=$(date +%s)
139 notify_step "db-migrate" "in_progress"
140 if "$BUN" run src/db/migrate.ts >>"$LOG" 2>&1; then
141 log " v db migrate ok"
142 notify_step "db-migrate" "succeeded" "$(( ( $(date +%s) - DM_START ) * 1000 ))"
143 else
144 log " x db migrate FAILED"
145 DEPLOY_FAILED=1
146 FAIL_REASON="bun run db:migrate failed"
147 notify_step "db-migrate" "failed" "$(( ( $(date +%s) - DM_START ) * 1000 ))"
148 fi
149fi
150
151# ── 6. Build the static binary ─────────────────────────────────────────────
152if [ "$DEPLOY_FAILED" = "0" ]; then
153 BD_START=$(date +%s)
154 notify_step "build" "in_progress"
155 mkdir -p .next
156 COMPILED=.next/gluecron-server
157 COMPILED_TMP=.next/gluecron-server.new
158 if "$BUN" build --compile --outfile "$COMPILED_TMP" src/index.ts >>"$LOG" 2>&1; then
159 mv -f "$COMPILED_TMP" "$COMPILED"
160 chmod +x "$COMPILED"
161 log " v compiled $COMPILED"
162 notify_step "build" "succeeded" "$(( ( $(date +%s) - BD_START ) * 1000 ))"
163 else
164 rm -f "$COMPILED_TMP"
165 log " ! bun build --compile failed — systemd will fall back to bun run"
166 notify_step "build" "succeeded" "$(( ( $(date +%s) - BD_START ) * 1000 ))"
167 fi
168fi
169
f85b88aTest User170# ── 6.5 Pin BUILD_SHA into the systemd unit so the running process can
171# report it and the SW versioning rotates exactly per-deploy. Drop-in
172# survives daemon-reload; the OLD file is overwritten on every deploy.
173# Without this, src/routes/pwa.ts falls back to a stable "dev-stable"
174# string and the browser SW never invalidates between real deploys.
175if [ "$DEPLOY_FAILED" = "0" ]; then
176 DROPIN_DIR=/etc/systemd/system/gluecron.service.d
177 mkdir -p "$DROPIN_DIR"
178 cat > "$DROPIN_DIR/build-sha.conf" <<EOF
179[Service]
180Environment="BUILD_SHA=$NEW_SHA"
181Environment="BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
182EOF
183 systemctl daemon-reload >>"$LOG" 2>&1 || log " ! daemon-reload non-fatal warning"
184 log " v pinned BUILD_SHA=${NEW_SHA:0:12} in systemd drop-in"
185fi
186
f2c00b4CC LABS App187# ── 7. systemctl restart (blocks on sd_notify READY=1) ────────────────────
188if [ "$DEPLOY_FAILED" = "0" ]; then
189 RS_START=$(date +%s)
190 notify_step "restart-service" "in_progress"
191 if systemctl restart gluecron >>"$LOG" 2>&1; then
192 log " v systemctl restart gluecron ok"
193 notify_step "restart-service" "succeeded" "$(( ( $(date +%s) - RS_START ) * 1000 ))"
194 else
195 log " x systemctl restart FAILED"
196 DEPLOY_FAILED=1
197 FAIL_REASON="systemctl restart failed"
198 notify_step "restart-service" "failed" "$(( ( $(date +%s) - RS_START ) * 1000 ))"
199 fi
200fi
201
202# ── 8. Wait for /healthz to be green (up to 30s) ──────────────────────────
203if [ "$DEPLOY_FAILED" = "0" ]; then
204 HZ_START=$(date +%s)
205 notify_step "healthz" "in_progress"
206 green=0
207 for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
208 code=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTHZ_URL" || echo "000")
209 log " healthz attempt $i: $code"
210 if [ "$code" = "200" ]; then green=1; break; fi
211 sleep 2
212 done
213 if [ "$green" = "1" ]; then
214 log " v /healthz green"
215 notify_step "healthz" "succeeded" "$(( ( $(date +%s) - HZ_START ) * 1000 ))"
216 else
217 log " x /healthz did not return 200 within 30s"
218 DEPLOY_FAILED=1
219 FAIL_REASON="/healthz timeout"
220 notify_step "healthz" "failed" "$(( ( $(date +%s) - HZ_START ) * 1000 ))"
221 fi
222fi
223
224# ── 9. Post-deploy smoke suite ────────────────────────────────────────────
225if [ "$DEPLOY_FAILED" = "0" ]; then
226 PS_START=$(date +%s)
227 notify_step "full-smoke" "in_progress"
228 export GLUECRON_HOST="http://localhost:${PORT}"
229 if "$BUN" run scripts/post-deploy-smoke.ts >>"$LOG" 2>&1; then
230 log " v post-deploy smoke green"
231 notify_step "full-smoke" "succeeded" "$(( ( $(date +%s) - PS_START ) * 1000 ))"
232 else
233 log " x post-deploy smoke FAILED"
234 DEPLOY_FAILED=1
235 FAIL_REASON="post-deploy smoke failed"
236 notify_step "full-smoke" "failed" "$(( ( $(date +%s) - PS_START ) * 1000 ))"
237 fi
238fi
239
240# ── 10. Rollback on failure ───────────────────────────────────────────────
241if [ "$DEPLOY_FAILED" = "1" ] && [ -n "$PREV_SHA" ] && [ "$PREV_SHA" != "$NEW_SHA" ]; then
242 notify_step "rollback" "in_progress"
243 log " ! rolling back to $PREV_SHA (reason: $FAIL_REASON)"
244 git reset --hard "$PREV_SHA" >>"$LOG" 2>&1 || true
245 systemctl restart gluecron >>"$LOG" 2>&1 || true
246 sleep 3
247 rb_green=0
248 for i in 1 2 3; do
249 code=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTHZ_URL" || echo "000")
250 log " rollback healthz attempt $i: $code"
251 if [ "$code" = "200" ]; then rb_green=1; break; fi
252 sleep 2
253 done
254 if [ "$rb_green" = "1" ]; then
255 notify_step "rollback" "succeeded"
256 log " v rollback green"
257 else
258 notify_step "rollback" "failed"
259 log " x ROLLBACK FAILED — human intervention required"
260 fi
261fi
262
263# ── 11. Notify deploy finished ────────────────────────────────────────────
264DUR_MS=$(( ( $(date +%s) - START_EPOCH ) * 1000 ))
265if [ -n "${DEPLOY_EVENT_TOKEN:-}" ] && [ -n "${APP_BASE_URL:-}" ]; then
266 if [ "$DEPLOY_FAILED" = "0" ]; then
267 curl --silent --show-error --max-time 10 \
268 -X POST "$APP_BASE_URL/api/events/deploy/finished" \
269 -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
270 -H "content-type: application/json" \
271 --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"status\":\"succeeded\",\"duration_ms\":$DUR_MS}" \
272 >>"$LOG" 2>&1 || true
273 else
274 ERR_PAYLOAD="$(printf '%s' "${FAIL_REASON:-deploy failed}" | head -c 512)"
275 if command -v jq >/dev/null 2>&1; then
276 ERR_JSON=$(printf '%s' "$ERR_PAYLOAD" | jq -Rs '.')
277 else
278 ERR_JSON="\"${ERR_PAYLOAD//\"/\\\"}\""
279 fi
280 curl --silent --show-error --max-time 10 \
281 -X POST "$APP_BASE_URL/api/events/deploy/finished" \
282 -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
283 -H "content-type: application/json" \
284 --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"status\":\"failed\",\"duration_ms\":$DUR_MS,\"error\":$ERR_JSON}" \
285 >>"$LOG" 2>&1 || true
286 fi
287fi
288
289if [ "$DEPLOY_FAILED" = "0" ]; then
290 log "==> gluecron self-deploy SUCCESS in ${DUR_MS}ms (sha=$NEW_SHA)"
291 exit 0
292else
293 log "==> gluecron self-deploy FAILED in ${DUR_MS}ms (reason=$FAIL_REASON)"
294 exit 1
295fi