-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·187 lines (163 loc) · 7.82 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·187 lines (163 loc) · 7.82 KB
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
#!/usr/bin/env bash
#
# Deploy GameHub to a VPS.
#
# - Default: build a release tar on the VPS, extract it to the release root,
# restart the systemd service. Brief downtime, in-progress games drop.
# - `--hot`: OTP hot-code upgrade (Castle/Forecastle) instead — generate a
# relup against the running version and install it into the LIVE node, so
# LiveView sockets survive. **Only safe for pure Elixir/rules changes.**
# - If the old release .rel file is missing (e.g. after a manual rollback or
# first run after switching to Castle), falls back to a full deploy.
#
# WHY FULL IS THE DEFAULT: `Phoenix.Endpoint` reads `cache_static_manifest`
# once, at boot. A relup that only does `{:load_module, Mod}` never re-inits the
# endpoint, so `~p"/assets/app.css"` keeps rendering the digest recorded in the
# manifest that was loaded at the last *restart* — and those digested files go
# out as `cache-control: immutable, max-age=31536000`. A hot upgrade therefore
# ships new Elixir code while every browser keeps the pre-deploy CSS/JS for a
# year. Anything touching assets/, priv/static/ or templates needs the restart.
#
# Version is read from mix.exs; bump it (and update appup.ex) before deploying.
# The easiest way is `make hooks` once — the pre-commit hook auto-bumps on main.
#
# Layout on the server:
# current repo <- source checkout where `mix release` runs (keeps _build)
# $BASE/app <- extracted release root; systemd runs $BASE/app/bin/game_hub
# .env <- optional runtime env loaded by config/runtime.exs
#
# Optional overrides:
# BASE (default /opt/game_hub)
# SERVICE (default game_hub)
# RESTART (default "sudo systemctl")
#
# Usage: ./deploy.sh [--hot] (--full is still accepted; it is now the default)
#
set -euo pipefail
cd "$(dirname "$0")"
BASE="${BASE:-/opt/game_hub}"
SERVICE="${SERVICE:-game_hub}"
RESTART="${RESTART:-sudo systemctl}"
APP_NAME="game_hub"
BUILD_DIR="$PWD"
APP_DIR="$BASE/app"
# Full restart is the default; hot upgrades are opt-in because they cannot
# refresh static assets (see the header comment).
WANT_HOT=0
[[ "${1:-}" == "--hot" ]] && WANT_HOT=1
green() { printf '\033[0;32m%s\033[0m\n' "$1"; }
red() { printf '\033[0;31m%s\033[0m\n' "$1" >&2; }
step() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; }
VSN="$(grep -m1 -E '^\s*version:' mix.exs | sed -E 's/.*"([^"]+)".*/\1/')"
[[ -n "$VSN" ]] || { red "Could not read version from mix.exs."; exit 1; }
green "Deploying $APP_NAME v$VSN to $BASE"
step "Checking local prerequisites"
mkdir -p "$APP_DIR"
for bin in mix elixir; do
command -v "$bin" >/dev/null || { red "Missing $bin on server (install Erlang/Elixir)."; exit 1; }
done
step "Building release v$VSN"
export MIX_ENV=prod
mix local.hex --force >/dev/null
mix local.rebar --force >/dev/null
mix deps.get --only prod
mix compile
mix assets.setup
mix assets.deploy
mix release --overwrite
# Determine the currently running version (empty if not running / not yet installed).
CUR_VSN="$(test -x "$APP_DIR/bin/game_hub" && \
"$APP_DIR/bin/game_hub" releases 2>/dev/null | \
awk '{ for (i=1;i<=NF;i++) if (tolower($i)=="permanent" && i>1) { print $(i-1); exit } }' \
|| true)"
CUR_VSN="$(echo "$CUR_VSN" | tr -d '[:space:]')"
# Decide deploy mode. Full unless --hot was asked for, and even then fall back
# to full when there is nothing to upgrade *from* (not running, or same version).
DEPLOY_MODE="full"
if [[ "$WANT_HOT" == "1" && -n "$CUR_VSN" && "$CUR_VSN" != "$VSN" ]]; then
DEPLOY_MODE="hot"
fi
if [[ "$DEPLOY_MODE" == "hot" ]]; then
OLD_REL="$APP_DIR/releases/${CUR_VSN}/${APP_NAME}.rel"
if [[ ! -f "$OLD_REL" ]]; then
red "Old .rel not found for $CUR_VSN ($OLD_REL) — falling back to full deploy."
DEPLOY_MODE="full"
fi
fi
TAR="$BUILD_DIR/_build/prod/${APP_NAME}-${VSN}.tar.gz"
[[ -f "$TAR" ]] || { red "Release tar not found: $TAR"; exit 1; }
if [[ "$DEPLOY_MODE" == "full" ]]; then
# ---------------------- FULL DEPLOY ----------------------------------------
if [[ "$CUR_VSN" == "$VSN" ]]; then
step "Full redeploy: refreshing v$VSN in $APP_DIR"
else
step "Full deploy: extracting v$VSN into $APP_DIR"
fi
tar xzf "$TAR" -C "$APP_DIR"
$RESTART restart "$SERVICE"
sleep 2
$RESTART --no-pager --full status "$SERVICE" | head -n 10 || true
green "Full deploy of v$VSN complete."
else
# ---------------------- HOT UPGRADE (relup, no restart) --------------------
step "Hot upgrade: $CUR_VSN -> $VSN (no restart)"
TARGET="$BUILD_DIR/_build/prod/rel/${APP_NAME}/releases/${VSN}/${APP_NAME}"
FROMTO="$APP_DIR/releases/${CUR_VSN}/${APP_NAME}"
echo "-> generating relup ${CUR_VSN} -> ${VSN}"
mix forecastle.relup --target "$TARGET" --fromto "$FROMTO" --outdir "$BUILD_DIR"
[[ -f "$BUILD_DIR/relup" ]] || { red "relup was not generated (check appup.ex)."; exit 1; }
echo "-> rebuilding release to embed relup"
mix release --overwrite >/dev/null
echo "-> unpacking new release"
cp "$TAR" "$APP_DIR/releases/${APP_NAME}-${VSN}.tar.gz"
"$APP_DIR/bin/${APP_NAME}" unpack "$VSN"
echo "-> installing into the running node"
"$APP_DIR/bin/${APP_NAME}" install "$VSN"
"$APP_DIR/bin/${APP_NAME}" commit "$VSN"
echo "-> current releases:"
"$APP_DIR/bin/${APP_NAME}" releases
rm -f "$BUILD_DIR/relup"
green "Hot upgrade to v$VSN complete (no downtime)."
fi
# Verify the running node actually serves the CSS we just built. This catches the
# silent failure mode described in the header: the endpoint keeps an old
# cache_static_manifest, so the HTML links a stale digest that is then cached
# `immutable` for a year. Non-fatal — the deploy is already live — but loud.
step "Verifying served assets"
EXPECTED_CSS="$(grep -o '"assets/css/app\.css":"[^"]*"' "$BUILD_DIR/priv/static/cache_manifest.json" 2>/dev/null \
| head -1 | sed -E 's/.*:"(.*)"/\1/')"
SERVED_CSS="$(curl -fsS --max-time 10 "http://127.0.0.1:${PORT:-4050}/" 2>/dev/null \
| grep -oE 'assets/css/app-[a-f0-9]+\.css' | head -1)"
if [[ -z "$EXPECTED_CSS" || -z "$SERVED_CSS" ]]; then
red "Could not verify served CSS (built='$EXPECTED_CSS' served='$SERVED_CSS') — check by hand."
elif [[ "$SERVED_CSS" == "$EXPECTED_CSS" ]]; then
green "Serving the freshly built CSS ($SERVED_CSS)."
else
red "STALE ASSETS: serving $SERVED_CSS but this build produced $EXPECTED_CSS."
red "The endpoint is still on an old cache_static_manifest. Run: ./deploy.sh"
fi
green "
App: https://${PHX_HOST:-games.gatetroy.com}
Logs: journalctl -u $SERVICE -f
"
# ------------------------------------------------------------------------------
# FIRST-TIME SERVER SETUP (run once, by hand)
# 1. Install Erlang/Elixir (match local OTP 27) and Node 18+.
# 2. sudo useradd -m -d /opt/game_hub -s /bin/bash game_hub
# sudo mkdir -p /opt/game_hub/{build,app} && sudo chown -R game_hub:game_hub /opt/game_hub
# 3. cp deploy/env.prod.example .env (edit; chmod 600;
# generate SECRET_KEY_BASE with `mix phx.gen.secret`)
# 4. sudo cp deploy/game_hub.service /etc/systemd/system/game_hub.service
# sudo systemctl daemon-reload && sudo systemctl enable game_hub
# 5. echo 'game_hub ALL=(root) NOPASSWD: /bin/systemctl restart game_hub, /bin/systemctl status game_hub' \
# | sudo tee /etc/sudoers.d/game_hub
# 6. Reverse proxy (nginx/Caddy) TLS for games.gatetroy.com -> 127.0.0.1:4050
# 7. Run once: make hooks (installs the pre-commit version-bump hook)
# 8. ./deploy.sh (first run does the full deploy + starts the service)
# Thereafter: commit your changes on main (hook bumps version) then ./deploy.sh
#
# No database, no email — game state lives only in each LiveView connection,
# so there's nothing to migrate. Hot upgrades (`--hot`) exist purely to avoid
# dropping players mid-game, and only work for pure Elixir changes — they cannot
# refresh static assets, so they are opt-in rather than the default.
# ------------------------------------------------------------------------------