diff --git a/deploy.sh b/deploy.sh index f1a9674..953b248 100755 --- a/deploy.sh +++ b/deploy.sh @@ -24,6 +24,7 @@ set -euo pipefail APP_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT_PATH="$APP_DIR/$(basename "$0")" cd "$APP_DIR" SERVICE="${HERMES_UI_SERVICE:-hermes-ui.service}" @@ -33,6 +34,10 @@ BACKUP_DIR="$STATE_DIR/backups" LOCK_HASH_FILE="$STATE_DIR/lockfile.sha256" KEEP_BACKUPS=3 +# gitea is not directly reachable from every host; when a plain fetch fails +# the script retries through the local mihomo proxy. +GIT_PROXY="${HERMES_UI_GIT_PROXY:-http://127.0.0.1:7890}" + HEALTH_URL="http://127.0.0.1:$PORT/health" AUTH_URL="http://127.0.0.1:$PORT/api/auth/status" @@ -104,6 +109,31 @@ while [ $# -gt 0 ]; do shift done +# --- Escape the service cgroup -------------------------------------------- +# +# This script restarts $SERVICE. When it is launched from inside that +# service's cgroup — e.g. from the agent bridge that the web UI itself +# runs — the restart SIGTERMs the whole cgroup, including this script. +# The build succeeds, the service comes back, but the script dies before it +# can run the post-restart health checks. +# +# Re-executing under a transient systemd scope moves the script into its +# own cgroup, so the restart no longer takes it down with it. A script +# started from a normal shell is not affected. + +if [ "${HERMES_UI_DEPLOY_SCOPED:-0}" != "1" ] \ + && [ -r /proc/self/cgroup ] \ + && grep -q "$SERVICE" /proc/self/cgroup 2>/dev/null; then + if command -v systemd-run >/dev/null 2>&1; then + scope_unit="hermes-ui-deploy-$(date +%s)-$$" + export HERMES_UI_DEPLOY_SCOPED=1 + echo "Running inside the $SERVICE cgroup — re-executing in scope $scope_unit" + exec systemd-run --scope --quiet --unit="$scope_unit" "$SCRIPT_PATH" "$@" + else + warn "systemd-run not found — the restart may kill this script early." + fi +fi + # --- Preflight ------------------------------------------------------------ step "Preflight" @@ -137,6 +167,30 @@ if [ "$DRY_RUN" -eq 1 ]; then fi # --- Pull ----------------------------------------------------------------- +# +# The deploy must always end up building the newest code on origin/main. +# Two things get in the way of that: +# +# * The branch diverged — the operator pushed from a dev machine while +# this host also had local commits. A merge commit would be recorded +# for every deploy; a rebase replays the local commits on top of +# origin/main and keeps the history linear. +# * origin is unreachable directly — gitea sits behind the network, so +# the fetch has to go through the local proxy. +# +# If the rebase hits a conflict the script stops instead of deploying a +# half-merged tree. + +fetch_origin() { + if git fetch --quiet origin main 2>/dev/null; then + return 0 + fi + if [ -n "$GIT_PROXY" ] && git -c "http.proxy=$GIT_PROXY" -c "https.proxy=$GIT_PROXY" \ + fetch --quiet origin main 2>/dev/null; then + return 0 + fi + return 1 +} if [ "$SKIP_PULL" -eq 1 ]; then step "Skipping git pull (--skip-pull)" @@ -149,10 +203,19 @@ else else # A failed fetch must not block a deploy: the point of the script is to # build and restart what is already checked out. - if ! git fetch --quiet origin main; then + if ! fetch_origin; then warn "Could not reach origin — continuing with the local checkout." elif git merge --ff-only --quiet origin/main; then ok "Updated to $(git rev-parse --short HEAD)" + elif [ -n "$(git rev-list --count origin/main..HEAD)" ]; then + # Diverged with local commits on top — rebase them onto origin/main. + step "Local commits found, rebasing onto origin/main" + if git rebase origin/main; then + ok "Rebased onto $(git rev-parse --short origin/main), now at $(git rev-parse --short HEAD)" + else + git rebase --abort || true + die "Rebase onto origin/main hit a conflict. Resolve it by hand and re-run." + fi else die "Fast-forward to origin/main failed. Resolve the branch by hand and re-run." fi