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