Mercurial
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" |