comparison hg-web/deploy.sh @ 249:c5129452493e

[deploy] Bundle Mercurial with hg-web Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 04:16:45 -0700
parents 8bb0ac8f4587
children
comparison
equal deleted inserted replaced
248:b8b6e726964a 249:c5129452493e
1 #!/usr/bin/env bash 1 #!/usr/bin/env bash
2 set -Eeuo pipefail 2 set -Eeuo pipefail
3 3
4 SERVICE_NAME="${SERVICE_NAME:-hg_web_server.service}" 4 SERVICE_NAME="${SERVICE_NAME:-hg_web_server.service}"
5 RELEASE_ROOT="${RELEASE_ROOT:-/opt/hg_web_server_releases}"
6 ACTIVE_PATH="${ACTIVE_PATH:-/opt/hg_web_server_bundle_active}"
7 HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:6970/}"
8 SERVICE_USER="${SERVICE_USER:-hg_web_server}" 5 SERVICE_USER="${SERVICE_USER:-hg_web_server}"
9 SERVICE_GROUP="${SERVICE_GROUP:-zenbu_team}" 6 SERVICE_GROUP="${SERVICE_GROUP:-zenbu_team}"
10 7 DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/hg_web_server}"
11 workspace="${BUILD_WORKSPACE_DIRECTORY:-$(hg root)}" 8 ACTIVE_PATH="${ACTIVE_PATH:-${DEPLOY_ROOT}/active}"
9 NEW_PATH="${NEW_PATH:-${DEPLOY_ROOT}/new}"
10 BACKUP_PATH="${BACKUP_PATH:-${DEPLOY_ROOT}/previous}"
11 REPOSITORY_PATH="${REPOSITORY_PATH:-${DEPLOY_ROOT}/repository}"
12 REPOSITORY_NEW_PATH="${REPOSITORY_NEW_PATH:-${DEPLOY_ROOT}/repository-new}"
13 HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:6970/}"
14 HG_ALLOW_PUSH="${HG_ALLOW_PUSH:-false}"
15 MANAGE_NGINX="${MANAGE_NGINX:-auto}"
16 NGINX_SERVER_NAME="${NGINX_SERVER_NAME:-zenbu.babocoder.com}"
17 NGINX_SITE_NAME="${NGINX_SITE_NAME:-hg-web}"
18 NGINX_SITE_AVAILABLE="${NGINX_SITE_AVAILABLE:-/etc/nginx/sites-available/${NGINX_SITE_NAME}}"
19 NGINX_SITE_ENABLED="${NGINX_SITE_ENABLED:-/etc/nginx/sites-enabled/${NGINX_SITE_NAME}}"
20
21 workspace="${BUILD_WORKSPACE_DIRECTORY:-$(cd "$(dirname "$0")/.." && pwd)}"
12 cd "$workspace" 22 cd "$workspace"
23 if [[ ! -d "$workspace/.hg" ]]; then
24 echo "Run deployment from a Zenbu Mercurial workspace." >&2
25 exit 1
26 fi
27
28 DEPLOY_ROOT="$(realpath -m "$DEPLOY_ROOT")"
29 ACTIVE_PATH="$(realpath -m "$ACTIVE_PATH")"
30 NEW_PATH="$(realpath -m "$NEW_PATH")"
31 BACKUP_PATH="$(realpath -m "$BACKUP_PATH")"
32 REPOSITORY_PATH="$(realpath -m "$REPOSITORY_PATH")"
33 REPOSITORY_NEW_PATH="$(realpath -m "$REPOSITORY_NEW_PATH")"
34 if [[ ! "$DEPLOY_ROOT" =~ ^/opt/[A-Za-z0-9._/-]+$ ]]; then
35 echo "DEPLOY_ROOT must be a dedicated directory below /opt." >&2
36 exit 1
37 fi
38 declare -A deployment_paths=()
39 for path in \
40 "$ACTIVE_PATH" \
41 "$NEW_PATH" \
42 "$BACKUP_PATH" \
43 "$REPOSITORY_PATH" \
44 "$REPOSITORY_NEW_PATH"; do
45 if [[ "$path" != "$DEPLOY_ROOT/"* ]]; then
46 echo "Deployment path escapes DEPLOY_ROOT: $path" >&2
47 exit 1
48 fi
49 if [[ -n "${deployment_paths[$path]:-}" ]]; then
50 echo "Deployment paths must be distinct: $path" >&2
51 exit 1
52 fi
53 deployment_paths["$path"]=1
54 done
55 if [[ ! "$SERVICE_USER" =~ ^[A-Za-z0-9._-]+$ ||
56 ! "$SERVICE_GROUP" =~ ^[A-Za-z0-9._-]+$ ||
57 ! "$SERVICE_NAME" =~ ^[A-Za-z0-9._@-]+$ ||
58 ! "$HG_ALLOW_PUSH" =~ ^(true|false)$ ]]; then
59 echo "Invalid systemd deployment setting." >&2
60 exit 1
61 fi
13 62
14 if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then 63 if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
15 if [[ -n "${RUNFILES_DIR:-}" ]]; then 64 if [[ -n "${RUNFILES_DIR:-}" ]]; then
16 source "$RUNFILES_DIR/bazel_tools/tools/bash/runfiles/runfiles.bash" 65 source "$RUNFILES_DIR/bazel_tools/tools/bash/runfiles/runfiles.bash"
17 elif [[ -n "${RUNFILES_MANIFEST_FILE:-}" ]]; then 66 elif [[ -n "${RUNFILES_MANIFEST_FILE:-}" ]]; then
35 else 84 else
36 echo "Bazel runfiles are unavailable." >&2 85 echo "Bazel runfiles are unavailable." >&2
37 exit 1 86 exit 1
38 fi 87 fi
39 bundle_dir="$(rlocation _main/hg-web/hg_web_server_bundle)" 88 bundle_dir="$(rlocation _main/hg-web/hg_web_server_bundle)"
89 nginx_template="$(rlocation _main/hg-web/nginx/hg-web.conf.template)"
90 service_template="$(rlocation _main/hg-web/systemd/hg-web.service.template)"
40 else 91 else
41 bazel build -c opt //hg-web:hg_web_server_bundle 92 bazel build -c opt //hg-web:hg_web_server_bundle
42 bundle_dir="bazel-bin/hg-web/hg_web_server_bundle" 93 bundle_dir="bazel-bin/hg-web/hg_web_server_bundle"
43 fi 94 nginx_template="hg-web/nginx/hg-web.conf.template"
44 95 service_template="hg-web/systemd/hg-web.service.template"
45 revision="$(hg log -r . -T '{node|short}')" 96 fi
46 release_name="${revision}-$(date -u +%Y%m%dT%H%M%SZ)" 97 NGINX_SITE_TEMPLATE="${NGINX_SITE_TEMPLATE:-$nginx_template}"
47 release_dir="${RELEASE_ROOT}/${release_name}" 98
48 staging_dir="${RELEASE_ROOT}/.${release_name}.tmp" 99 rendered_nginx="$(mktemp)"
49 next_link="${ACTIVE_PATH}.next" 100 rendered_service="$(mktemp)"
50 previous_release="" 101 service_backup="$(mktemp)"
51 promoted=0 102 repository_bundle="$(mktemp)"
103 release_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
104 promotion_prepared=0
105 service_changed=0
106 service_had_previous=0
107 service_was_enabled=0
108 nginx_site_created=0
109 nginx_link_created=0
110 lock_acquired=0
111
112 cleanup() {
113 rm -f \
114 "$rendered_nginx" \
115 "$rendered_service" \
116 "$service_backup" \
117 "$repository_bundle"
118 }
52 119
53 health_check() { 120 health_check() {
54 for _ in $(seq 1 20); do 121 for _ in $(seq 1 20); do
55 if curl --fail --silent --max-time 3 "$HEALTH_URL" >/dev/null; then 122 if curl --fail --silent --max-time 3 "$HEALTH_URL" >/dev/null; then
56 return 0 123 return 0
57 fi 124 fi
58 sleep 1 125 sleep 1
59 done 126 done
60 echo "Health check failed: $HEALTH_URL" >&2
61 return 1 127 return 1
62 } 128 }
63 129
64 point_active_at() { 130 restore_host_configuration() {
65 local target="$1" 131 local restore_failed=0
66 sudo rm -f "$next_link" 132
67 sudo ln -s "$target" "$next_link" 133 if [[ "$nginx_link_created" -eq 1 ]]; then
68 sudo mv -Tf "$next_link" "$ACTIVE_PATH" 134 sudo rm -f "$NGINX_SITE_ENABLED" || restore_failed=1
135 fi
136 if [[ "$nginx_site_created" -eq 1 ]]; then
137 sudo rm -f "$NGINX_SITE_AVAILABLE" || restore_failed=1
138 fi
139 if [[ "$nginx_link_created" -eq 1 || "$nginx_site_created" -eq 1 ]]; then
140 if sudo nginx -t; then
141 sudo systemctl reload nginx || restore_failed=1
142 else
143 restore_failed=1
144 fi
145 fi
146
147 if [[ "$service_changed" -eq 1 ]]; then
148 if [[ "$service_had_previous" -eq 1 ]]; then
149 sudo install -o root -g root -m 0644 \
150 "$service_backup" "/etc/systemd/system/$SERVICE_NAME" ||
151 restore_failed=1
152 else
153 sudo rm -f "/etc/systemd/system/$SERVICE_NAME" ||
154 restore_failed=1
155 fi
156 sudo systemctl daemon-reload || restore_failed=1
157 if [[ "$service_was_enabled" -eq 1 ]]; then
158 sudo systemctl enable "$SERVICE_NAME" >/dev/null ||
159 restore_failed=1
160 else
161 sudo systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true
162 fi
163 fi
164
165 if [[ "$restore_failed" -ne 0 ]]; then
166 echo "Host configuration rollback needs manual attention." >&2
167 fi
168 return 0
69 } 169 }
70 170
71 rollback() { 171 rollback() {
72 trap - ERR 172 trap - ERR INT TERM EXIT
73 if [[ "$promoted" -eq 1 && -n "$previous_release" && -d "$previous_release" ]]; then 173 if [[ "$lock_acquired" -eq 1 ]]; then
74 echo "Deployment failed; rolling back to $previous_release" >&2 174 restore_host_configuration
75 point_active_at "$previous_release" 175 if [[ "$promotion_prepared" -eq 1 ]]; then
76 sudo systemctl restart "$SERVICE_NAME" 176 if [[ -d "$BACKUP_PATH" ]]; then
77 health_check || echo "Rollback completed, but the health check still fails." >&2 177 sudo rm -rf "$ACTIVE_PATH"
78 else 178 sudo mv "$BACKUP_PATH" "$ACTIVE_PATH"
79 echo "Deployment failed and no previous release is available for rollback." >&2 179 sudo systemctl restart "$SERVICE_NAME" || true
80 fi 180 elif sudo test -f "$ACTIVE_PATH/.hg-web-release" &&
81 sudo rm -rf "$staging_dir" 181 [[ "$(sudo cat "$ACTIVE_PATH/.hg-web-release")" == "$release_id" ]]; then
182 sudo rm -rf "$ACTIVE_PATH"
183 sudo systemctl stop "$SERVICE_NAME" || true
184 fi
185 fi
186 sudo rm -rf "$NEW_PATH"
187 sudo rm -rf "$REPOSITORY_NEW_PATH"
188 fi
189 cleanup
82 exit 1 190 exit 1
83 } 191 }
84 trap rollback ERR 192 trap rollback ERR INT TERM
85 193 trap cleanup EXIT
86 sudo install -d -o root -g "$SERVICE_GROUP" -m 0755 "$RELEASE_ROOT" 194
87 sudo rm -rf "$staging_dir" 195 deploy_user="$(id -un)"
88 sudo install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$staging_dir" 196 sudo install -d -o "$deploy_user" -g "$SERVICE_GROUP" -m 0755 "$DEPLOY_ROOT"
89 sudo cp -a "${bundle_dir}/." "$staging_dir/" 197 exec 9>"$DEPLOY_ROOT/deploy.lock"
90 sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$staging_dir" 198 if ! flock -n 9; then
91 199 echo "Another hg-web deployment is already running." >&2
92 sudo test -x "$staging_dir/hg_web_server" 200 exit 1
93 sudo test -f "$staging_dir/hg-web/src/index.html" 201 fi
94 sudo test -f "$staging_dir/hg-web/src/page.js" 202 lock_acquired=1
95 sudo mv "$staging_dir" "$release_dir" 203 sudo rm -rf "$NEW_PATH"
96 204 sudo cp -a "$bundle_dir" "$NEW_PATH"
97 if [[ -L "$ACTIVE_PATH" ]]; then 205 sudo chown -R "$deploy_user:$SERVICE_GROUP" "$NEW_PATH"
98 previous_release="$(readlink -f "$ACTIVE_PATH")" 206 printf '%s\n' "$release_id" > "$NEW_PATH/.hg-web-release"
99 elif [[ -d "$ACTIVE_PATH" ]]; then 207
100 previous_release="${RELEASE_ROOT}/legacy-$(date -u +%Y%m%dT%H%M%SZ)" 208 bundled_hg="$NEW_PATH/third_party/mercurial/runtime/bin/hg"
101 sudo mv "$ACTIVE_PATH" "$previous_release" 209 test -x "$NEW_PATH/hg_web_server"
102 fi 210 test -x "$NEW_PATH/hg-web/run_hg_web"
103 211 test -x "$bundled_hg"
104 point_active_at "$release_dir" 212
105 promoted=1 213 if [[ -e "$REPOSITORY_PATH" && ! -d "$REPOSITORY_PATH/.hg" ]]; then
214 echo "REPOSITORY_PATH exists but is not a Mercurial repository." >&2
215 exit 1
216 fi
217 if [[ ! -d "$REPOSITORY_PATH/.hg" ]]; then
218 sudo rm -rf "$REPOSITORY_NEW_PATH"
219 HGRCPATH= "$bundled_hg" clone \
220 --noupdate --pull "$workspace" "$REPOSITORY_NEW_PATH"
221 sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$REPOSITORY_NEW_PATH"
222 sudo mv "$REPOSITORY_NEW_PATH" "$REPOSITORY_PATH"
223 else
224 HGRCPATH= "$bundled_hg" --repository "$workspace" \
225 bundle --all "$repository_bundle"
226 sudo chown "$deploy_user:$SERVICE_GROUP" "$repository_bundle"
227 chmod 0640 "$repository_bundle"
228 sudo -u "$SERVICE_USER" env HGRCPATH= \
229 "$bundled_hg" --repository "$REPOSITORY_PATH" \
230 pull "$repository_bundle"
231 fi
232 sudo -u "$SERVICE_USER" env HGRCPATH= \
233 "$bundled_hg" --repository "$REPOSITORY_PATH" verify
234
235 sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$NEW_PATH"
236
237 sed \
238 -e "s|__SERVICE_USER__|${SERVICE_USER}|g" \
239 -e "s|__SERVICE_GROUP__|${SERVICE_GROUP}|g" \
240 -e "s|__ACTIVE_PATH__|${ACTIVE_PATH}|g" \
241 -e "s|__HG_ALLOW_PUSH__|${HG_ALLOW_PUSH}|g" \
242 -e "s|__REPOSITORY_PATH__|${REPOSITORY_PATH}|g" \
243 "$service_template" > "$rendered_service"
244 if sudo test -f "/etc/systemd/system/$SERVICE_NAME"; then
245 sudo cat "/etc/systemd/system/$SERVICE_NAME" > "$service_backup"
246 chmod 0600 "$service_backup"
247 service_had_previous=1
248 fi
249 if sudo systemctl is-enabled --quiet "$SERVICE_NAME"; then
250 service_was_enabled=1
251 fi
252 service_changed=1
253 sudo install -o root -g root -m 0644 \
254 "$rendered_service" "/etc/systemd/system/$SERVICE_NAME"
255 sudo systemctl daemon-reload
256
257 manage_nginx=0
258 case "$MANAGE_NGINX" in
259 1|true|yes) manage_nginx=1 ;;
260 0|false|no) ;;
261 auto)
262 if command -v nginx >/dev/null 2>&1 &&
263 [[ -d /etc/nginx/sites-available && -d /etc/nginx/sites-enabled ]]; then
264 manage_nginx=1
265 fi
266 ;;
267 *)
268 echo "MANAGE_NGINX must be auto, true, or false." >&2
269 exit 1
270 ;;
271 esac
272
273 if [[ "$manage_nginx" -eq 1 &&
274 ! -e "$NGINX_SITE_AVAILABLE" &&
275 ! -L "$NGINX_SITE_AVAILABLE" ]]; then
276 if [[ ! "$NGINX_SERVER_NAME" =~ ^[A-Za-z0-9.-]+$ ]]; then
277 echo "Invalid NGINX_SERVER_NAME: $NGINX_SERVER_NAME" >&2
278 exit 1
279 fi
280 if [[ ! -f "$NGINX_SITE_TEMPLATE" ]]; then
281 echo "Nginx template not found: $NGINX_SITE_TEMPLATE" >&2
282 exit 1
283 fi
284 sed "s/__SERVER_NAME__/${NGINX_SERVER_NAME}/g" \
285 "$NGINX_SITE_TEMPLATE" > "$rendered_nginx"
286 sudo install -D -o root -g root -m 0644 \
287 "$rendered_nginx" "$NGINX_SITE_AVAILABLE"
288 nginx_site_created=1
289 sudo ln -s "$NGINX_SITE_AVAILABLE" "$NGINX_SITE_ENABLED"
290 nginx_link_created=1
291 sudo nginx -t
292 sudo systemctl reload nginx
293 elif [[ "$manage_nginx" -eq 1 &&
294 ! -e "$NGINX_SITE_ENABLED" &&
295 ! -L "$NGINX_SITE_ENABLED" ]]; then
296 sudo ln -s "$NGINX_SITE_AVAILABLE" "$NGINX_SITE_ENABLED"
297 nginx_link_created=1
298 sudo nginx -t
299 sudo systemctl reload nginx
300 elif [[ "$manage_nginx" -eq 1 ]]; then
301 echo "Nginx site already exists; leaving it unchanged."
302 fi
303
304 sudo rm -rf "$BACKUP_PATH"
305 promotion_prepared=1
306 if [[ -e "$ACTIVE_PATH" || -L "$ACTIVE_PATH" ]]; then
307 sudo mv "$ACTIVE_PATH" "$BACKUP_PATH"
308 fi
309 sudo mv "$NEW_PATH" "$ACTIVE_PATH"
310
311 sudo systemctl enable "$SERVICE_NAME" >/dev/null
106 sudo systemctl restart "$SERVICE_NAME" 312 sudo systemctl restart "$SERVICE_NAME"
107 health_check 313 if ! health_check; then
108 314 echo "hg-web health check failed." >&2
109 trap - ERR 315 rollback
110 echo "Deployment complete: $release_dir" 316 fi
317
318 sudo rm -rf "$BACKUP_PATH"
319 trap - ERR INT TERM EXIT
320 cleanup
321 echo "Deployment complete: $ACTIVE_PATH"