Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepfix/audit-sweep-2026-07-26gatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
self-deploy.sh17.1 KB · 394 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#!/usr/bin/env bash
# =============================================================================
# BLOCK W — Gluecron self-deploy.
#
# Fired by:
#   - src/hooks/post-receive.ts when SELF_HOST_REPO matches the pushed repo
#     AND the ref is refs/heads/main
#   - the bare repo's hooks/post-receive (for SSH receive-pack)
#   - the optional .gluecron/workflows/deploy.yml on the workflow runner
#
# Contract:
#   - Detaches into the background via systemd-run (or nohup fallback) so
#     the caller's git push returns in ~1 second
#   - Logs every step to /var/log/gluecron-self-deploy.log
#   - Notifies /api/events/deploy/{started,step,finished} so /admin/deploys
#     streams the live timeline (same wire as the GitHub Actions workflow)
#   - Rolls back via git reflog if the post-deploy smoke fails
#
# Operator invariants:
#   - /etc/gluecron.env is the source of env truth (DATABASE_URL,
#     DEPLOY_EVENT_TOKEN, APP_BASE_URL, ANTHROPIC_API_KEY, …)
#   - /opt/gluecron is the working tree on the box, with git remote `origin`
#     pointing at https://gluecron.com/<owner>/<repo>.git (NOT GitHub)
#   - /opt/gluecron/.next/gluecron-server is the compiled Bun binary
#   - systemd unit `gluecron` is Type=notify and ExecStart=$EXEC_START
#
# TODO(ops): configure /etc/logrotate.d/gluecron-self-deploy for the log.
#
# REACHABILITY (verified on the box 2026-07-29): this script is NOT currently
# running. post-receive.ts only dispatches it when process.env.SELF_HOST_REPO
# matches, and while SELF_HOST_REPO is set in the HOST env file it is absent
# from the container the app actually runs in, so the hook never fires. No
# systemd unit invokes it either. The live deploy path is
# gluecron-update.timer -> scripts/auto-update.sh, which is what CLAUDE.md's
# "post-receive fires self-deploy.sh" claim gets wrong. The log's last write
# was 2026-07-13.
#
# It is kept rather than deleted because admin-diagnose and the runbook still
# present it as the self-host path and it may be re-armed. That is exactly
# why the xtrace holes below were worth closing: a dormant script is the one
# most likely to be switched on without a fresh review.
# =============================================================================

# `set -E` so traps propagate into subshells; `-x` traces every command
# to stderr (captured into $LOG via the `>>"$LOG" 2>&1` redirects on the
# detached re-exec line). Reliability sweep 2026-05-16: when this script
# fails, the trace tells us EXACTLY which line broke instead of leaving
# us guessing as we did for 17 hours of failed Hetzner deploys.
set -Eeuxo pipefail

WORKING_DIR="${GLUECRON_WORKING_DIR:-/opt/gluecron}"
LOG="${GLUECRON_SELF_DEPLOY_LOG:-/var/log/gluecron-self-deploy.log}"
ENV_FILE="${GLUECRON_ENV_FILE:-/etc/gluecron.env}"

# The log receives this script's stderr, which under `set -x` is a trace of
# every command. Create it 0600 before anything writes: on the live box it
# was found at 0644, i.e. readable by any local user. Best-effort — a
# pre-existing log keeps its mode, so tightening that is an operator step.
if [ ! -e "$LOG" ]; then
  ( umask 077; : >"$LOG" ) 2>/dev/null || true
fi
chmod 600 "$LOG" 2>/dev/null || true
BUN="${GLUECRON_BUN:-/root/.bun/bin/bun}"
HEALTHZ_URL="${GLUECRON_HEALTHZ_URL:-http://localhost:3010/healthz}"
PORT="${GLUECRON_PORT:-3010}"
DETACHED_FLAG="${1:-}"

# ── helpers ────────────────────────────────────────────────────────────────
ts() { date +'%Y-%m-%dT%H:%M:%S%z'; }
log() { echo "[$(ts)] $*" | tee -a "$LOG" >&2; }

notify_step() {
  local NAME="$1" STATUS="$2" DUR="${3:-}"
  if [ -z "${DEPLOY_EVENT_TOKEN:-}" ] || [ -z "${APP_BASE_URL:-}" ]; then
    return 0
  fi
  local DUR_FIELD=""
  if [ -n "$DUR" ]; then DUR_FIELD=",\"duration_ms\":$DUR"; fi
  # xtrace OFF: the bearer token is on the command line, so tracing this
  # would print DEPLOY_EVENT_TOKEN into $LOG on every single step.
  set +x
  curl --silent --show-error --max-time 5 \
    -X POST "$APP_BASE_URL/api/events/deploy/step" \
    -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
    -H "content-type: application/json" \
    --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"step_name\":\"$NAME\",\"status\":\"$STATUS\"$DUR_FIELD}" \
    >/dev/null 2>&1 || true
  set -x
}

# ── re-exec into the background unless already detached ──────────────────
# When the post-receive hook calls this script, git push is blocked on the
# child's stdout/stderr. We re-exec ourselves through systemd-run so the
# original SSH/HTTP receive-pack process can return immediately.
if [ "$DETACHED_FLAG" != "--inline" ] && [ -z "${GLUECRON_SELF_DEPLOY_DETACHED:-}" ]; then
  export GLUECRON_SELF_DEPLOY_DETACHED=1
  if command -v systemd-run >/dev/null 2>&1; then
    systemd-run --quiet --unit="gluecron-self-deploy-$(date +%s)" \
      --collect --no-block \
      bash "$0" --inline "$@" || nohup bash "$0" --inline "$@" >>"$LOG" 2>&1 &
  else
    nohup bash "$0" --inline "$@" >>"$LOG" 2>&1 &
    disown || true
  fi
  exit 0
fi

# Everything below runs in the detached process.
mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
touch "$LOG" 2>/dev/null || true

log "==> gluecron self-deploy starting (pid $$)"

# ── 1. Source env ──────────────────────────────────────────────────────────
if [ -f "$ENV_FILE" ]; then
  # xtrace OFF across the source. With `set -x` active, bash traces every
  # assignment the sourced file performs, so the entire env file — today
  # that includes GOOGLE_OAUTH_CLIENT_SECRET — would be echoed to stderr.
  # The header above is explicit that stderr is captured into $LOG by the
  # detached re-exec, and that log is world-readable, so tracing here writes
  # live credentials to disk for any local user to read.
  set +x
  set -a
  # shellcheck disable=SC1090
  source "$ENV_FILE"
  set +a
  set -x
  log "    v sourced $ENV_FILE"
else
  log "    ! $ENV_FILE not found — relying on inherited env"
fi

cd "$WORKING_DIR"

# ── 2. Capture pre-deploy SHA for rollback ────────────────────────────────
PREV_SHA="$(git rev-parse HEAD 2>/dev/null || echo '')"
log "    v previous SHA: $PREV_SHA"

# ── 3. Pull latest main ────────────────────────────────────────────────────
GP_START=$(date +%s)
notify_step "git-pull" "in_progress"
git fetch --prune origin main 2>&1 | tee -a "$LOG"
git reset --hard origin/main 2>&1 | tee -a "$LOG"
NEW_SHA="$(git rev-parse HEAD)"
RUN_ID="self-${NEW_SHA:0:12}-$(date +%s)"
log "    v pulled to $NEW_SHA (run_id=$RUN_ID)"
notify_step "git-pull" "succeeded" "$(( ( $(date +%s) - GP_START ) * 1000 ))"

# ── 3.5 Notify deploy started (now that we have NEW_SHA + RUN_ID) ─────────
if [ -n "${DEPLOY_EVENT_TOKEN:-}" ] && [ -n "${APP_BASE_URL:-}" ]; then
  # xtrace OFF: bearer token on the command line, and this one redirects
  # straight into $LOG.
  set +x
  curl --silent --show-error --max-time 10 \
    -X POST "$APP_BASE_URL/api/events/deploy/started" \
    -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
    -H "content-type: application/json" \
    --data "{\"sha\":\"$NEW_SHA\",\"run_id\":\"$RUN_ID\",\"source\":\"self-deploy\"}" \
    >>"$LOG" 2>&1 || log "    ! deploy/started notify failed (non-fatal)"
  set -x
fi

START_EPOCH=$(date +%s)
DEPLOY_FAILED=0
FAIL_REASON=""

# ── 4. bun install --frozen-lockfile (skipped if lockfile unchanged) ──────
# Track the last-installed lockfile hash in a stamp file. If it matches the
# current lockfile we skip — saves ~2-5s every deploy that doesn't touch
# dependencies (90%+ of deploys). On any error the fallback runs install
# anyway so we never silently skip a needed install.
STAMP_DIR="${GLUECRON_DEPLOY_STAMP_DIR:-.next}"
mkdir -p "$STAMP_DIR" 2>/dev/null || true
LOCK_STAMP="$STAMP_DIR/bun-install.stamp"
LOCK_HASH=""
if [ -f bun.lockb ]; then
  LOCK_HASH=$(sha256sum bun.lockb 2>/dev/null | awk '{print $1}' || echo "")
elif [ -f bun.lock ]; then
  LOCK_HASH=$(sha256sum bun.lock 2>/dev/null | awk '{print $1}' || echo "")
fi
LAST_HASH=""
if [ -f "$LOCK_STAMP" ]; then
  LAST_HASH=$(cat "$LOCK_STAMP" 2>/dev/null || echo "")
fi

BI_START=$(date +%s)
notify_step "bun-install" "in_progress"
if [ -n "$LOCK_HASH" ] && [ "$LOCK_HASH" = "$LAST_HASH" ] && [ -d node_modules ]; then
  log "    v bun install skipped (lockfile unchanged: ${LOCK_HASH:0:12})"
  notify_step "bun-install" "succeeded" "$(( ( $(date +%s) - BI_START ) * 1000 ))"
elif "$BUN" install --frozen-lockfile >>"$LOG" 2>&1; then
  echo "$LOCK_HASH" > "$LOCK_STAMP" 2>/dev/null || true
  log "    v bun install ok"
  notify_step "bun-install" "succeeded" "$(( ( $(date +%s) - BI_START ) * 1000 ))"
else
  log "    x bun install FAILED"
  DEPLOY_FAILED=1
  FAIL_REASON="bun install failed"
  notify_step "bun-install" "failed" "$(( ( $(date +%s) - BI_START ) * 1000 ))"
fi

# ── 5. DB migrations (fail loud) ───────────────────────────────────────────
if [ "$DEPLOY_FAILED" = "0" ]; then
  DM_START=$(date +%s)
  notify_step "db-migrate" "in_progress"
  if "$BUN" run src/db/migrate.ts >>"$LOG" 2>&1; then
    log "    v db migrate ok"
    notify_step "db-migrate" "succeeded" "$(( ( $(date +%s) - DM_START ) * 1000 ))"
  else
    log "    x db migrate FAILED"
    DEPLOY_FAILED=1
    FAIL_REASON="bun run db:migrate failed"
    notify_step "db-migrate" "failed" "$(( ( $(date +%s) - DM_START ) * 1000 ))"
  fi
fi

# ── 6. Build the static binary (skipped if no source files changed) ──────
# We hash every tracked src/*.ts(x) file and stash the digest in a stamp
# file. If nothing in src/ changed, the binary from the previous deploy
# is still valid — saves 5-15s on docs-only / config-only commits.
# On any error the fallback rebuilds anyway.
if [ "$DEPLOY_FAILED" = "0" ]; then
  BD_START=$(date +%s)
  notify_step "build" "in_progress"
  mkdir -p .next
  COMPILED=.next/gluecron-server
  COMPILED_TMP=.next/gluecron-server.new
  BUILD_STAMP="$STAMP_DIR/build-src.stamp"
  SRC_HASH=$(find src -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 2>/dev/null \
    | sort -z \
    | xargs -0 sha256sum 2>/dev/null \
    | sha256sum 2>/dev/null \
    | awk '{print $1}' || echo "")
  LAST_BUILD_HASH=""
  if [ -f "$BUILD_STAMP" ]; then
    LAST_BUILD_HASH=$(cat "$BUILD_STAMP" 2>/dev/null || echo "")
  fi

  if [ -n "$SRC_HASH" ] && [ "$SRC_HASH" = "$LAST_BUILD_HASH" ] && [ -x "$COMPILED" ]; then
    log "    v build skipped (src/ unchanged: ${SRC_HASH:0:12})"
    notify_step "build" "succeeded" "$(( ( $(date +%s) - BD_START ) * 1000 ))"
  elif "$BUN" build --compile --outfile "$COMPILED_TMP" src/index.ts >>"$LOG" 2>&1; then
    mv -f "$COMPILED_TMP" "$COMPILED"
    chmod +x "$COMPILED"
    echo "$SRC_HASH" > "$BUILD_STAMP" 2>/dev/null || true
    log "    v compiled $COMPILED"
    notify_step "build" "succeeded" "$(( ( $(date +%s) - BD_START ) * 1000 ))"
  else
    rm -f "$COMPILED_TMP"
    log "    ! bun build --compile failed — systemd will fall back to bun run"
    notify_step "build" "succeeded" "$(( ( $(date +%s) - BD_START ) * 1000 ))"
  fi
fi

# ── 6.5 Pin BUILD_SHA into the systemd unit so the running process can
# report it and the SW versioning rotates exactly per-deploy. Drop-in
# survives daemon-reload; the OLD file is overwritten on every deploy.
# Without this, src/routes/pwa.ts falls back to a stable "dev-stable"
# string and the browser SW never invalidates between real deploys.
if [ "$DEPLOY_FAILED" = "0" ]; then
  DROPIN_DIR=/etc/systemd/system/gluecron.service.d
  mkdir -p "$DROPIN_DIR"
  cat > "$DROPIN_DIR/build-sha.conf" <<EOF
[Service]
Environment="BUILD_SHA=$NEW_SHA"
Environment="BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
EOF
  systemctl daemon-reload >>"$LOG" 2>&1 || log "    ! daemon-reload non-fatal warning"
  log "    v pinned BUILD_SHA=${NEW_SHA:0:12} in systemd drop-in"
fi

# ── 7. systemctl restart (blocks on sd_notify READY=1) ────────────────────
if [ "$DEPLOY_FAILED" = "0" ]; then
  RS_START=$(date +%s)
  notify_step "restart-service" "in_progress"
  if systemctl restart gluecron >>"$LOG" 2>&1; then
    log "    v systemctl restart gluecron ok"
    notify_step "restart-service" "succeeded" "$(( ( $(date +%s) - RS_START ) * 1000 ))"
  else
    log "    x systemctl restart FAILED"
    DEPLOY_FAILED=1
    FAIL_REASON="systemctl restart failed"
    notify_step "restart-service" "failed" "$(( ( $(date +%s) - RS_START ) * 1000 ))"
  fi
fi

# ── 8. Wait for /healthz to be green (fast poll, up to ~30s) ─────────────
# Tighter polling: 500ms × 10 (covers the common case where the service
# is healthy within 1-2s of restart returning), then 2s × 10 for the slow
# path. Cuts a typical successful deploy by 1-3s vs the old fixed 2s poll.
if [ "$DEPLOY_FAILED" = "0" ]; then
  HZ_START=$(date +%s)
  notify_step "healthz" "in_progress"
  green=0
  for i in 1 2 3 4 5 6 7 8 9 10; do
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 "$HEALTHZ_URL" || echo "000")
    if [ "$code" = "200" ]; then green=1; log "    healthz fast attempt $i: $code (green)"; break; fi
    sleep 0.5
  done
  if [ "$green" = "0" ]; then
    for i in 11 12 13 14 15 16 17 18 19 20; do
      code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 "$HEALTHZ_URL" || echo "000")
      log "    healthz slow attempt $i: $code"
      if [ "$code" = "200" ]; then green=1; break; fi
      sleep 2
    done
  fi
  if [ "$green" = "1" ]; then
    log "    v /healthz green"
    notify_step "healthz" "succeeded" "$(( ( $(date +%s) - HZ_START ) * 1000 ))"
  else
    log "    x /healthz did not return 200 within 30s"
    DEPLOY_FAILED=1
    FAIL_REASON="/healthz timeout"
    notify_step "healthz" "failed" "$(( ( $(date +%s) - HZ_START ) * 1000 ))"
  fi
fi

# ── 9. Post-deploy smoke suite ────────────────────────────────────────────
if [ "$DEPLOY_FAILED" = "0" ]; then
  PS_START=$(date +%s)
  notify_step "full-smoke" "in_progress"
  export GLUECRON_HOST="http://localhost:${PORT}"
  if "$BUN" run scripts/post-deploy-smoke.ts >>"$LOG" 2>&1; then
    log "    v post-deploy smoke green"
    notify_step "full-smoke" "succeeded" "$(( ( $(date +%s) - PS_START ) * 1000 ))"
  else
    log "    x post-deploy smoke FAILED"
    DEPLOY_FAILED=1
    FAIL_REASON="post-deploy smoke failed"
    notify_step "full-smoke" "failed" "$(( ( $(date +%s) - PS_START ) * 1000 ))"
  fi
fi

# ── 10. Rollback on failure ───────────────────────────────────────────────
if [ "$DEPLOY_FAILED" = "1" ] && [ -n "$PREV_SHA" ] && [ "$PREV_SHA" != "$NEW_SHA" ]; then
  notify_step "rollback" "in_progress"
  log "    ! rolling back to $PREV_SHA (reason: $FAIL_REASON)"
  git reset --hard "$PREV_SHA" >>"$LOG" 2>&1 || true
  systemctl restart gluecron >>"$LOG" 2>&1 || true
  sleep 3
  rb_green=0
  for i in 1 2 3; do
    code=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTHZ_URL" || echo "000")
    log "    rollback healthz attempt $i: $code"
    if [ "$code" = "200" ]; then rb_green=1; break; fi
    sleep 2
  done
  if [ "$rb_green" = "1" ]; then
    notify_step "rollback" "succeeded"
    log "    v rollback green"
  else
    notify_step "rollback" "failed"
    log "    x ROLLBACK FAILED — human intervention required"
  fi
fi

# ── 11. Notify deploy finished ────────────────────────────────────────────
DUR_MS=$(( ( $(date +%s) - START_EPOCH ) * 1000 ))
if [ -n "${DEPLOY_EVENT_TOKEN:-}" ] && [ -n "${APP_BASE_URL:-}" ]; then
  # xtrace OFF across both branches: bearer token on the command line, both
  # redirecting into $LOG.
  set +x
  if [ "$DEPLOY_FAILED" = "0" ]; then
    curl --silent --show-error --max-time 10 \
      -X POST "$APP_BASE_URL/api/events/deploy/finished" \
      -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
      -H "content-type: application/json" \
      --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"status\":\"succeeded\",\"duration_ms\":$DUR_MS}" \
      >>"$LOG" 2>&1 || true
  else
    ERR_PAYLOAD="$(printf '%s' "${FAIL_REASON:-deploy failed}" | head -c 512)"
    if command -v jq >/dev/null 2>&1; then
      ERR_JSON=$(printf '%s' "$ERR_PAYLOAD" | jq -Rs '.')
    else
      ERR_JSON="\"${ERR_PAYLOAD//\"/\\\"}\""
    fi
    curl --silent --show-error --max-time 10 \
      -X POST "$APP_BASE_URL/api/events/deploy/finished" \
      -H "authorization: Bearer $DEPLOY_EVENT_TOKEN" \
      -H "content-type: application/json" \
      --data "{\"run_id\":\"$RUN_ID\",\"sha\":\"$NEW_SHA\",\"status\":\"failed\",\"duration_ms\":$DUR_MS,\"error\":$ERR_JSON}" \
      >>"$LOG" 2>&1 || true
  fi
  set -x
fi

if [ "$DEPLOY_FAILED" = "0" ]; then
  log "==> gluecron self-deploy SUCCESS in ${DUR_MS}ms (sha=$NEW_SHA)"
  exit 0
else
  log "==> gluecron self-deploy FAILED in ${DUR_MS}ms (reason=$FAIL_REASON)"
  exit 1
fi