diff 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
line wrap: on
line diff
--- a/hg-web/deploy.sh	Tue Aug 04 02:44:06 2026 -0700
+++ b/hg-web/deploy.sh	Tue Aug 04 04:16:45 2026 -0700
@@ -2,14 +2,63 @@
 set -Eeuo pipefail
 
 SERVICE_NAME="${SERVICE_NAME:-hg_web_server.service}"
-RELEASE_ROOT="${RELEASE_ROOT:-/opt/hg_web_server_releases}"
-ACTIVE_PATH="${ACTIVE_PATH:-/opt/hg_web_server_bundle_active}"
-HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:6970/}"
 SERVICE_USER="${SERVICE_USER:-hg_web_server}"
 SERVICE_GROUP="${SERVICE_GROUP:-zenbu_team}"
+DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/hg_web_server}"
+ACTIVE_PATH="${ACTIVE_PATH:-${DEPLOY_ROOT}/active}"
+NEW_PATH="${NEW_PATH:-${DEPLOY_ROOT}/new}"
+BACKUP_PATH="${BACKUP_PATH:-${DEPLOY_ROOT}/previous}"
+REPOSITORY_PATH="${REPOSITORY_PATH:-${DEPLOY_ROOT}/repository}"
+REPOSITORY_NEW_PATH="${REPOSITORY_NEW_PATH:-${DEPLOY_ROOT}/repository-new}"
+HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:6970/}"
+HG_ALLOW_PUSH="${HG_ALLOW_PUSH:-false}"
+MANAGE_NGINX="${MANAGE_NGINX:-auto}"
+NGINX_SERVER_NAME="${NGINX_SERVER_NAME:-zenbu.babocoder.com}"
+NGINX_SITE_NAME="${NGINX_SITE_NAME:-hg-web}"
+NGINX_SITE_AVAILABLE="${NGINX_SITE_AVAILABLE:-/etc/nginx/sites-available/${NGINX_SITE_NAME}}"
+NGINX_SITE_ENABLED="${NGINX_SITE_ENABLED:-/etc/nginx/sites-enabled/${NGINX_SITE_NAME}}"
 
-workspace="${BUILD_WORKSPACE_DIRECTORY:-$(hg root)}"
+workspace="${BUILD_WORKSPACE_DIRECTORY:-$(cd "$(dirname "$0")/.." && pwd)}"
 cd "$workspace"
+if [[ ! -d "$workspace/.hg" ]]; then
+  echo "Run deployment from a Zenbu Mercurial workspace." >&2
+  exit 1
+fi
+
+DEPLOY_ROOT="$(realpath -m "$DEPLOY_ROOT")"
+ACTIVE_PATH="$(realpath -m "$ACTIVE_PATH")"
+NEW_PATH="$(realpath -m "$NEW_PATH")"
+BACKUP_PATH="$(realpath -m "$BACKUP_PATH")"
+REPOSITORY_PATH="$(realpath -m "$REPOSITORY_PATH")"
+REPOSITORY_NEW_PATH="$(realpath -m "$REPOSITORY_NEW_PATH")"
+if [[ ! "$DEPLOY_ROOT" =~ ^/opt/[A-Za-z0-9._/-]+$ ]]; then
+  echo "DEPLOY_ROOT must be a dedicated directory below /opt." >&2
+  exit 1
+fi
+declare -A deployment_paths=()
+for path in \
+  "$ACTIVE_PATH" \
+  "$NEW_PATH" \
+  "$BACKUP_PATH" \
+  "$REPOSITORY_PATH" \
+  "$REPOSITORY_NEW_PATH"; do
+  if [[ "$path" != "$DEPLOY_ROOT/"* ]]; then
+    echo "Deployment path escapes DEPLOY_ROOT: $path" >&2
+    exit 1
+  fi
+  if [[ -n "${deployment_paths[$path]:-}" ]]; then
+    echo "Deployment paths must be distinct: $path" >&2
+    exit 1
+  fi
+  deployment_paths["$path"]=1
+done
+if [[ ! "$SERVICE_USER" =~ ^[A-Za-z0-9._-]+$ ||
+      ! "$SERVICE_GROUP" =~ ^[A-Za-z0-9._-]+$ ||
+      ! "$SERVICE_NAME" =~ ^[A-Za-z0-9._@-]+$ ||
+      ! "$HG_ALLOW_PUSH" =~ ^(true|false)$ ]]; then
+  echo "Invalid systemd deployment setting." >&2
+  exit 1
+fi
 
 if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
   if [[ -n "${RUNFILES_DIR:-}" ]]; then
@@ -37,18 +86,36 @@
     exit 1
   fi
   bundle_dir="$(rlocation _main/hg-web/hg_web_server_bundle)"
+  nginx_template="$(rlocation _main/hg-web/nginx/hg-web.conf.template)"
+  service_template="$(rlocation _main/hg-web/systemd/hg-web.service.template)"
 else
   bazel build -c opt //hg-web:hg_web_server_bundle
   bundle_dir="bazel-bin/hg-web/hg_web_server_bundle"
+  nginx_template="hg-web/nginx/hg-web.conf.template"
+  service_template="hg-web/systemd/hg-web.service.template"
 fi
+NGINX_SITE_TEMPLATE="${NGINX_SITE_TEMPLATE:-$nginx_template}"
 
-revision="$(hg log -r . -T '{node|short}')"
-release_name="${revision}-$(date -u +%Y%m%dT%H%M%SZ)"
-release_dir="${RELEASE_ROOT}/${release_name}"
-staging_dir="${RELEASE_ROOT}/.${release_name}.tmp"
-next_link="${ACTIVE_PATH}.next"
-previous_release=""
-promoted=0
+rendered_nginx="$(mktemp)"
+rendered_service="$(mktemp)"
+service_backup="$(mktemp)"
+repository_bundle="$(mktemp)"
+release_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
+promotion_prepared=0
+service_changed=0
+service_had_previous=0
+service_was_enabled=0
+nginx_site_created=0
+nginx_link_created=0
+lock_acquired=0
+
+cleanup() {
+  rm -f \
+    "$rendered_nginx" \
+    "$rendered_service" \
+    "$service_backup" \
+    "$repository_bundle"
+}
 
 health_check() {
   for _ in $(seq 1 20); do
@@ -57,54 +124,198 @@
     fi
     sleep 1
   done
-  echo "Health check failed: $HEALTH_URL" >&2
   return 1
 }
 
-point_active_at() {
-  local target="$1"
-  sudo rm -f "$next_link"
-  sudo ln -s "$target" "$next_link"
-  sudo mv -Tf "$next_link" "$ACTIVE_PATH"
+restore_host_configuration() {
+  local restore_failed=0
+
+  if [[ "$nginx_link_created" -eq 1 ]]; then
+    sudo rm -f "$NGINX_SITE_ENABLED" || restore_failed=1
+  fi
+  if [[ "$nginx_site_created" -eq 1 ]]; then
+    sudo rm -f "$NGINX_SITE_AVAILABLE" || restore_failed=1
+  fi
+  if [[ "$nginx_link_created" -eq 1 || "$nginx_site_created" -eq 1 ]]; then
+    if sudo nginx -t; then
+      sudo systemctl reload nginx || restore_failed=1
+    else
+      restore_failed=1
+    fi
+  fi
+
+  if [[ "$service_changed" -eq 1 ]]; then
+    if [[ "$service_had_previous" -eq 1 ]]; then
+      sudo install -o root -g root -m 0644 \
+        "$service_backup" "/etc/systemd/system/$SERVICE_NAME" ||
+        restore_failed=1
+    else
+      sudo rm -f "/etc/systemd/system/$SERVICE_NAME" ||
+        restore_failed=1
+    fi
+    sudo systemctl daemon-reload || restore_failed=1
+    if [[ "$service_was_enabled" -eq 1 ]]; then
+      sudo systemctl enable "$SERVICE_NAME" >/dev/null ||
+        restore_failed=1
+    else
+      sudo systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true
+    fi
+  fi
+
+  if [[ "$restore_failed" -ne 0 ]]; then
+    echo "Host configuration rollback needs manual attention." >&2
+  fi
+  return 0
 }
 
 rollback() {
-  trap - ERR
-  if [[ "$promoted" -eq 1 && -n "$previous_release" && -d "$previous_release" ]]; then
-    echo "Deployment failed; rolling back to $previous_release" >&2
-    point_active_at "$previous_release"
-    sudo systemctl restart "$SERVICE_NAME"
-    health_check || echo "Rollback completed, but the health check still fails." >&2
-  else
-    echo "Deployment failed and no previous release is available for rollback." >&2
+  trap - ERR INT TERM EXIT
+  if [[ "$lock_acquired" -eq 1 ]]; then
+    restore_host_configuration
+    if [[ "$promotion_prepared" -eq 1 ]]; then
+      if [[ -d "$BACKUP_PATH" ]]; then
+        sudo rm -rf "$ACTIVE_PATH"
+        sudo mv "$BACKUP_PATH" "$ACTIVE_PATH"
+        sudo systemctl restart "$SERVICE_NAME" || true
+      elif sudo test -f "$ACTIVE_PATH/.hg-web-release" &&
+           [[ "$(sudo cat "$ACTIVE_PATH/.hg-web-release")" == "$release_id" ]]; then
+        sudo rm -rf "$ACTIVE_PATH"
+        sudo systemctl stop "$SERVICE_NAME" || true
+      fi
+    fi
+    sudo rm -rf "$NEW_PATH"
+    sudo rm -rf "$REPOSITORY_NEW_PATH"
   fi
-  sudo rm -rf "$staging_dir"
+  cleanup
   exit 1
 }
-trap rollback ERR
+trap rollback ERR INT TERM
+trap cleanup EXIT
+
+deploy_user="$(id -un)"
+sudo install -d -o "$deploy_user" -g "$SERVICE_GROUP" -m 0755 "$DEPLOY_ROOT"
+exec 9>"$DEPLOY_ROOT/deploy.lock"
+if ! flock -n 9; then
+  echo "Another hg-web deployment is already running." >&2
+  exit 1
+fi
+lock_acquired=1
+sudo rm -rf "$NEW_PATH"
+sudo cp -a "$bundle_dir" "$NEW_PATH"
+sudo chown -R "$deploy_user:$SERVICE_GROUP" "$NEW_PATH"
+printf '%s\n' "$release_id" > "$NEW_PATH/.hg-web-release"
+
+bundled_hg="$NEW_PATH/third_party/mercurial/runtime/bin/hg"
+test -x "$NEW_PATH/hg_web_server"
+test -x "$NEW_PATH/hg-web/run_hg_web"
+test -x "$bundled_hg"
 
-sudo install -d -o root -g "$SERVICE_GROUP" -m 0755 "$RELEASE_ROOT"
-sudo rm -rf "$staging_dir"
-sudo install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$staging_dir"
-sudo cp -a "${bundle_dir}/." "$staging_dir/"
-sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$staging_dir"
+if [[ -e "$REPOSITORY_PATH" && ! -d "$REPOSITORY_PATH/.hg" ]]; then
+  echo "REPOSITORY_PATH exists but is not a Mercurial repository." >&2
+  exit 1
+fi
+if [[ ! -d "$REPOSITORY_PATH/.hg" ]]; then
+  sudo rm -rf "$REPOSITORY_NEW_PATH"
+  HGRCPATH= "$bundled_hg" clone \
+    --noupdate --pull "$workspace" "$REPOSITORY_NEW_PATH"
+  sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$REPOSITORY_NEW_PATH"
+  sudo mv "$REPOSITORY_NEW_PATH" "$REPOSITORY_PATH"
+else
+  HGRCPATH= "$bundled_hg" --repository "$workspace" \
+    bundle --all "$repository_bundle"
+  sudo chown "$deploy_user:$SERVICE_GROUP" "$repository_bundle"
+  chmod 0640 "$repository_bundle"
+  sudo -u "$SERVICE_USER" env HGRCPATH= \
+    "$bundled_hg" --repository "$REPOSITORY_PATH" \
+    pull "$repository_bundle"
+fi
+sudo -u "$SERVICE_USER" env HGRCPATH= \
+  "$bundled_hg" --repository "$REPOSITORY_PATH" verify
+
+sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$NEW_PATH"
 
-sudo test -x "$staging_dir/hg_web_server"
-sudo test -f "$staging_dir/hg-web/src/index.html"
-sudo test -f "$staging_dir/hg-web/src/page.js"
-sudo mv "$staging_dir" "$release_dir"
+sed \
+  -e "s|__SERVICE_USER__|${SERVICE_USER}|g" \
+  -e "s|__SERVICE_GROUP__|${SERVICE_GROUP}|g" \
+  -e "s|__ACTIVE_PATH__|${ACTIVE_PATH}|g" \
+  -e "s|__HG_ALLOW_PUSH__|${HG_ALLOW_PUSH}|g" \
+  -e "s|__REPOSITORY_PATH__|${REPOSITORY_PATH}|g" \
+  "$service_template" > "$rendered_service"
+if sudo test -f "/etc/systemd/system/$SERVICE_NAME"; then
+  sudo cat "/etc/systemd/system/$SERVICE_NAME" > "$service_backup"
+  chmod 0600 "$service_backup"
+  service_had_previous=1
+fi
+if sudo systemctl is-enabled --quiet "$SERVICE_NAME"; then
+  service_was_enabled=1
+fi
+service_changed=1
+sudo install -o root -g root -m 0644 \
+  "$rendered_service" "/etc/systemd/system/$SERVICE_NAME"
+sudo systemctl daemon-reload
 
-if [[ -L "$ACTIVE_PATH" ]]; then
-  previous_release="$(readlink -f "$ACTIVE_PATH")"
-elif [[ -d "$ACTIVE_PATH" ]]; then
-  previous_release="${RELEASE_ROOT}/legacy-$(date -u +%Y%m%dT%H%M%SZ)"
-  sudo mv "$ACTIVE_PATH" "$previous_release"
+manage_nginx=0
+case "$MANAGE_NGINX" in
+  1|true|yes) manage_nginx=1 ;;
+  0|false|no) ;;
+  auto)
+    if command -v nginx >/dev/null 2>&1 &&
+       [[ -d /etc/nginx/sites-available && -d /etc/nginx/sites-enabled ]]; then
+      manage_nginx=1
+    fi
+    ;;
+  *)
+    echo "MANAGE_NGINX must be auto, true, or false." >&2
+    exit 1
+    ;;
+esac
+
+if [[ "$manage_nginx" -eq 1 &&
+      ! -e "$NGINX_SITE_AVAILABLE" &&
+      ! -L "$NGINX_SITE_AVAILABLE" ]]; then
+  if [[ ! "$NGINX_SERVER_NAME" =~ ^[A-Za-z0-9.-]+$ ]]; then
+    echo "Invalid NGINX_SERVER_NAME: $NGINX_SERVER_NAME" >&2
+    exit 1
+  fi
+  if [[ ! -f "$NGINX_SITE_TEMPLATE" ]]; then
+    echo "Nginx template not found: $NGINX_SITE_TEMPLATE" >&2
+    exit 1
+  fi
+  sed "s/__SERVER_NAME__/${NGINX_SERVER_NAME}/g" \
+    "$NGINX_SITE_TEMPLATE" > "$rendered_nginx"
+  sudo install -D -o root -g root -m 0644 \
+    "$rendered_nginx" "$NGINX_SITE_AVAILABLE"
+  nginx_site_created=1
+  sudo ln -s "$NGINX_SITE_AVAILABLE" "$NGINX_SITE_ENABLED"
+  nginx_link_created=1
+  sudo nginx -t
+  sudo systemctl reload nginx
+elif [[ "$manage_nginx" -eq 1 &&
+        ! -e "$NGINX_SITE_ENABLED" &&
+        ! -L "$NGINX_SITE_ENABLED" ]]; then
+  sudo ln -s "$NGINX_SITE_AVAILABLE" "$NGINX_SITE_ENABLED"
+  nginx_link_created=1
+  sudo nginx -t
+  sudo systemctl reload nginx
+elif [[ "$manage_nginx" -eq 1 ]]; then
+  echo "Nginx site already exists; leaving it unchanged."
 fi
 
-point_active_at "$release_dir"
-promoted=1
+sudo rm -rf "$BACKUP_PATH"
+promotion_prepared=1
+if [[ -e "$ACTIVE_PATH" || -L "$ACTIVE_PATH" ]]; then
+  sudo mv "$ACTIVE_PATH" "$BACKUP_PATH"
+fi
+sudo mv "$NEW_PATH" "$ACTIVE_PATH"
+
+sudo systemctl enable "$SERVICE_NAME" >/dev/null
 sudo systemctl restart "$SERVICE_NAME"
-health_check
+if ! health_check; then
+  echo "hg-web health check failed." >&2
+  rollback
+fi
 
-trap - ERR
-echo "Deployment complete: $release_dir"
+sudo rm -rf "$BACKUP_PATH"
+trap - ERR INT TERM EXIT
+cleanup
+echo "Deployment complete: $ACTIVE_PATH"