werkator build failed after 01:46
RFC 0005 step 2. `restartsInstance` marks the build that installs the version the instance is to run next. On SUCCESS the executor drains — nothing new starts, everything executing is finished and recorded — and then closes the server context, so Restart=always brings the new version up. Until now the restart was scheduled from outside and asked whether the instance happened to be idle, so a build starting between the answer and the stop was cut short and re-run by the recovery. The key is pinned like the sandbox policy, and refused on builds.default and on a definition without trigger.branches: a restart must never come from whatever branch went green last. tools/self-deploy reads WERKATOR_RESTARTS_INSTANCE from its environment and hands the running MainPID to the transient unit, which waits for that pid to go instead of polling for idleness. The stop afterwards keeps Restart=always from bringing the old runtime back mid-swap; the swap and the rollback stay outside, where PR#64 put them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #65
459 lines
22 KiB
Bash
Executable File
459 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Installs a Werkator runtime bundle into the instance that is running this very
|
|
# script, and asks systemd — from outside the service — to restart it (RFC 0005,
|
|
# step 1).
|
|
#
|
|
# This is the `buildCommand` of the repository's `deploy` build: a follow-up of
|
|
# the test build on main, run natively (never in the sandbox, it has to reach the
|
|
# installation directory). Werkator is the one repository its own instance builds
|
|
# but cannot deploy the ordinary way, because the deploy build is a child of the
|
|
# process it replaces: the stop ends the build, the build is recorded INTERRUPTED,
|
|
# and the startup recovery re-runs it — a loop, not a glitch. So the build only
|
|
# stages, verifies and swaps the runtime, and schedules the restart as a transient
|
|
# systemd unit. That unit lives outside the service's control group, fires after
|
|
# the build is recorded SUCCESS, and the recovery therefore leaves it alone.
|
|
#
|
|
# Since step 2 the instance ends itself: a build with `restartsInstance` drains its
|
|
# queue and closes its context once nothing executes any more, and it exports
|
|
# WERKATOR_RESTARTS_INSTANCE=true into this script's environment so it knows. The
|
|
# transient unit is then no longer what stops the service — it waits for the JVM to
|
|
# go, swaps into the gap that `RestartSec` leaves, and starts the new version. The
|
|
# unit stays either way, because the rollback needs it: an instance that does not
|
|
# start cannot put the previous runtime back itself.
|
|
#
|
|
# Usage:
|
|
# tools/self-deploy deploy build: stage and verify, then let the
|
|
# transient unit swap and restart
|
|
# tools/self-deploy --no-restart stage, verify and swap at once, leaving the
|
|
# restart to the caller
|
|
# tools/self-deploy --install-dir DIR installation directory to swap in (default:
|
|
# derived from the running unit's ExecStart)
|
|
# tools/self-deploy --self-restart-pid PID (transient unit only) wait for that pid to
|
|
# go instead of stopping the instance
|
|
# tools/self-deploy --bundle SRC the runtime bundle, as the directory
|
|
# build/runtime-bundle/werkator or as a
|
|
# tarball (default: the directory in this
|
|
# worktree, else the tarball)
|
|
#
|
|
# Given both an installation directory and a bundle it swaps and nothing else —
|
|
# no git worktree, no systemd, no running instance to ask. That is how tools/remote
|
|
# installs a bundle from a developer machine, where the service is stopped and
|
|
# started around the swap by that script itself.
|
|
#
|
|
# Everything else it derives from where it runs:
|
|
# - the primary checkout from `git rev-parse --git-common-dir`, and from its
|
|
# basename the unit name exactly as SystemdServiceFiles.unitName does;
|
|
# - the installation directory from that unit's ExecStart, whose jar sits in
|
|
# <install>/werkator/lib/;
|
|
# - the instance's port from <install>/werkator/bin/werkator config:print.
|
|
#
|
|
# The build never installs anything. It puts the new runtime beside the running one
|
|
# as `werkator.new` and runs its launcher — which is what catches a jlinked JRE that
|
|
# does not start on this host — and there it stops. The transient unit does the rest
|
|
# while the service is down: stop, swap, start. That order matters, because replacing
|
|
# the directory under a running JVM leaves it pointing at a foreign `jspawnhelper`,
|
|
# and from that moment it cannot start a single child process — a watcher that fetches
|
|
# nothing and a build that runs nothing (PR#64).
|
|
#
|
|
# The runtime that is replaced stays as `werkator.prev`: the last known good version,
|
|
# and what the unit puts back when the restarted service does not answer.
|
|
#
|
|
# A self-deployment takes the bundle as a directory, because it was built on this
|
|
# very host: packing 64 MiB and unpacking it seconds later would be work for
|
|
# nobody. A tarball is what a transfer from another machine needs, and both are
|
|
# accepted here.
|
|
|
|
set -euo pipefail
|
|
|
|
# Only the head start the transient unit needs to come up. It does not have to cover
|
|
# the deploy build any more: the unit waits for the instance to go idle, and a build
|
|
# is gone from the running list only after its status and artifacts are written — the
|
|
# executor guarantees that order. What used to be a guess of thirty seconds is now a
|
|
# condition, and it is met as soon as it is true (PR#64).
|
|
RESTART_DELAY_SECONDS=5
|
|
# How long the unit waits for the instance to go idle before restarting anyway. A
|
|
# build interrupted then is re-enqueued by the startup recovery, so the deployment
|
|
# is never lost — but it is not free either, hence the generous bound.
|
|
IDLE_TIMEOUT_SECONDS=600
|
|
IDLE_INTERVAL_SECONDS=5
|
|
# How long the watchdog waits for the restarted instance to answer before it
|
|
# considers the new version broken and puts the previous one back.
|
|
HEALTH_TIMEOUT_SECONDS=120
|
|
HEALTH_INTERVAL_SECONDS=5
|
|
BUNDLE_DIR_RELATIVE_PATH="build/runtime-bundle/werkator"
|
|
BUNDLE_TARBALL_RELATIVE_PATH="build/distributions/werkator-runtime-linux-x64.tar.gz"
|
|
# The copy of this script that the transient unit runs; it lives in the
|
|
# installation, which is per instance, so the name needs no suffix.
|
|
WATCHDOG_NAME="werkator-self-restart"
|
|
|
|
die() {
|
|
echo "ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
awk 'NR > 2 && !/^#/ { exit } NR > 2 { sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"
|
|
exit 2
|
|
}
|
|
|
|
# systemctl --user and systemd-run need the user manager's bus. The deploy build
|
|
# is a child of the service, so it normally inherits both; a manual run from a
|
|
# non-login shell (or a cron-like context) does not, and guessing the default
|
|
# paths is what every systemd user unit does anyway.
|
|
ensure_user_bus() {
|
|
: "${XDG_RUNTIME_DIR:=/run/user/$(id -u)}"
|
|
export XDG_RUNTIME_DIR
|
|
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && [ -S "$XDG_RUNTIME_DIR/bus" ]; then
|
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
|
|
fi
|
|
command -v systemctl >/dev/null 2>&1 ||
|
|
die "no systemctl on this host — self-deployment needs a systemd user manager (RFC 0005 step 1)"
|
|
systemctl --user show --property=Version >/dev/null 2>&1 ||
|
|
die "no systemd user manager reachable (XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR) — self-deployment needs one"
|
|
}
|
|
|
|
# Mirrors SystemdServiceFiles.unitName: the repository directory's name, every
|
|
# character outside [A-Za-z0-9_.-] replaced by a dash.
|
|
unit_name_for() {
|
|
echo "werkator-$(basename "$1" | sed 's/[^A-Za-z0-9_.-]/-/g').service"
|
|
}
|
|
|
|
# The transient unit that restarts and verifies, named after the service it
|
|
# restarts: transient unit names are global to the user manager, and two instances
|
|
# under one account deploying at the same time must not collide on one name.
|
|
restart_unit_for() {
|
|
echo "werkator-self-restart-$(basename "$1" .service | sed 's/^werkator-//')"
|
|
}
|
|
|
|
# The primary checkout of the repository this build belongs to — the directory the
|
|
# service watches, and the one that names its unit. In a build worktree the common
|
|
# git dir is the primary's `.git`; in the primary checkout itself it is `.git` too,
|
|
# which is why the caller checks for a linked worktree separately.
|
|
primary_checkout() {
|
|
local common_dir
|
|
common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" ||
|
|
die "not inside a git repository — run this as the deploy build's command"
|
|
case "$common_dir" in
|
|
/*) ;;
|
|
*) common_dir="$PWD/$common_dir" ;;
|
|
esac
|
|
dirname "$(cd "$common_dir" && pwd)"
|
|
}
|
|
|
|
# A build runs in .git/werkator/worktrees/<branchKey>, never in the primary
|
|
# checkout — and a deployment that swapped the runtime out from under the
|
|
# repository it was started from would be exactly the accident worktrees exist to
|
|
# prevent.
|
|
require_linked_worktree() {
|
|
local git_dir common_dir
|
|
git_dir="$(git rev-parse --absolute-git-dir 2>/dev/null)" ||
|
|
die "not inside a git repository"
|
|
common_dir="$(git rev-parse --git-common-dir 2>/dev/null)"
|
|
case "$common_dir" in
|
|
/*) ;;
|
|
*) common_dir="$PWD/$common_dir" ;;
|
|
esac
|
|
common_dir="$(cd "$common_dir" && pwd)"
|
|
[ "$git_dir" != "$common_dir" ] ||
|
|
die "this is the primary checkout — the deploy build runs in a worktree under .git/werkator/worktrees/"
|
|
}
|
|
|
|
# The installation directory, read from the unit that is actually running: its
|
|
# ExecStart names the jar, which sits in <install>/werkator/lib/werkator.jar.
|
|
install_dir_from_unit() {
|
|
local unit="$1" exec_start jar bundle_dir
|
|
exec_start="$(systemctl --user show -p ExecStart --value "$unit" 2>/dev/null || true)"
|
|
[ -n "$exec_start" ] ||
|
|
die "the unit $unit is not known to the user manager — is this the host the instance runs on?"
|
|
jar="$(printf '%s\n' "$exec_start" | tr ' ' '\n' | grep -m1 '\.jar$' || true)"
|
|
[ -n "$jar" ] || die "cannot read a jar path from the ExecStart of $unit: $exec_start"
|
|
bundle_dir="$(dirname "$(dirname "$jar")")"
|
|
[ "$(basename "$bundle_dir")" = "werkator" ] &&
|
|
[ -x "$bundle_dir/bin/werkator" ] &&
|
|
[ -d "$bundle_dir/jre" ] ||
|
|
die "$unit does not run a runtime bundle (jar: $jar) — self-deployment installs bundles only, see docs/deployment.md"
|
|
dirname "$bundle_dir"
|
|
}
|
|
|
|
# The port the instance serves on, straight from the installed binary: config:print
|
|
# is the single answer to "what is configured", wherever the key lives.
|
|
port_of_instance() {
|
|
local install_dir="$1" repo_dir="$2" port
|
|
port="$(cd "$repo_dir" && "$install_dir/werkator/bin/werkator" config:print 2>/dev/null |
|
|
awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"' || true)"
|
|
[ -n "$port" ] || die "cannot determine the server port from $install_dir/werkator/bin/werkator config:print"
|
|
echo "$port"
|
|
}
|
|
|
|
# Unpacks beside the running runtime and proves the new launcher starts on this host.
|
|
# Nothing of the installation is touched: what this leaves behind is `werkator.new`.
|
|
stage_and_verify() {
|
|
local install_dir="$1" bundle="$2" staging version
|
|
staging="$install_dir/.werkator-stage"
|
|
echo "==> Staging $bundle in $install_dir"
|
|
rm -rf "$staging" "$install_dir/werkator.new"
|
|
if [ -d "$bundle" ]; then
|
|
# already unpacked, built on this host by the preceding build
|
|
cp -a "$bundle" "$install_dir/werkator.new"
|
|
else
|
|
mkdir -p "$staging"
|
|
tar xzf "$bundle" -C "$staging"
|
|
[ -d "$staging/werkator" ] ||
|
|
die "the bundle does not contain a top-level werkator/ directory: $bundle"
|
|
mv "$staging/werkator" "$install_dir/werkator.new"
|
|
rmdir "$staging"
|
|
fi
|
|
|
|
echo "==> Verifying the staged runtime natively"
|
|
version="$("$install_dir/werkator.new/bin/werkator" --version 2>&1)" || {
|
|
echo "$version" >&2
|
|
rm -rf "$install_dir/werkator.new"
|
|
die "the staged runtime does not run on this host — nothing was swapped"
|
|
}
|
|
echo " $version"
|
|
}
|
|
|
|
# Puts the staged runtime in place. The unit's ExecStart keeps pointing at the same
|
|
# path, because only the directory's content changes, never its name — which is also
|
|
# why this must not happen while that JVM is running.
|
|
swap_staged() {
|
|
local install_dir="$1"
|
|
[ -d "$install_dir/werkator.new" ] || die "nothing staged in $install_dir — werkator.new is missing"
|
|
echo "==> Swapping in the staged runtime (the replaced one stays as werkator.prev)"
|
|
rm -rf "$install_dir/werkator.prev"
|
|
[ ! -d "$install_dir/werkator" ] || mv "$install_dir/werkator" "$install_dir/werkator.prev"
|
|
mv "$install_dir/werkator.new" "$install_dir/werkator"
|
|
}
|
|
|
|
# Answers once the instance serves again; the watcher endpoint is the cheapest
|
|
# route that proves the server context came up, not just the port.
|
|
instance_answers() {
|
|
local port="$1"
|
|
curl -fsS --max-time 5 "http://127.0.0.1:$port/api/watcher" >/dev/null 2>&1
|
|
}
|
|
|
|
await_instance() {
|
|
local port="$1" waited=0
|
|
while [ "$waited" -lt "$HEALTH_TIMEOUT_SECONDS" ]; do
|
|
if instance_answers "$port"; then
|
|
return 0
|
|
fi
|
|
sleep "$HEALTH_INTERVAL_SECONDS"
|
|
waited=$((waited + HEALTH_INTERVAL_SECONDS))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# True while the instance has a build running or queued, over all of its repositories.
|
|
# An unreachable instance runs nothing, so it counts as idle.
|
|
instance_busy() {
|
|
local port="$1" body
|
|
body="$(curl -fsS --max-time 10 "http://127.0.0.1:$port/api/repos" 2>/dev/null)" || return 1
|
|
printf '%s' "$body" | grep -qE '"(running|pending)":[1-9]'
|
|
}
|
|
|
|
# Waits for the deploy build that scheduled this unit to be recorded, and for
|
|
# anything else to finish with it. A build leaves the running list only after its
|
|
# status and its artifacts are written — that order is an invariant of the executor —
|
|
# so an idle instance means the deployment is safely in the history and the restart
|
|
# cannot turn it into an interrupted build that deploys all over again.
|
|
await_idle() {
|
|
local port="$1" waited=0
|
|
while [ "$waited" -lt "$IDLE_TIMEOUT_SECONDS" ]; do
|
|
if ! instance_busy "$port"; then
|
|
[ "$waited" -eq 0 ] || echo "instance idle after ${waited}s"
|
|
return 0
|
|
fi
|
|
sleep "$IDLE_INTERVAL_SECONDS"
|
|
waited=$((waited + IDLE_INTERVAL_SECONDS))
|
|
done
|
|
echo "instance still busy after ${IDLE_TIMEOUT_SECONDS}s — restarting anyway; a running build is re-enqueued by the recovery" >&2
|
|
return 0
|
|
}
|
|
|
|
# The pid of the running instance, 0 while the service is down. Passed to the
|
|
# transient unit at schedule time, so that waiting for "this instance is gone" is a
|
|
# comparison and not a guess: the pid read afterwards is either still the one that
|
|
# scheduled the restart, 0 while it is down, or already the next one.
|
|
main_pid_of() {
|
|
systemctl --user show -p MainPID --value "$1" 2>/dev/null || echo 0
|
|
}
|
|
|
|
# Waits for the instance that scheduled this unit to end itself (RFC 0005, step 2).
|
|
# Nothing is stopped here: the executor drains its queue, records every build that was
|
|
# still executing, and only then closes its context — so the moment the pid changes,
|
|
# the deployment is in the history and no build is being cut short. `RestartSec=30`
|
|
# then leaves the gap the swap needs.
|
|
await_instance_exit() {
|
|
local unit="$1" was="$2" waited=0
|
|
while [ "$waited" -lt "$IDLE_TIMEOUT_SECONDS" ]; do
|
|
if [ "$(main_pid_of "$unit")" != "$was" ]; then
|
|
echo "the instance ended itself after ${waited}s (was pid $was)"
|
|
return 0
|
|
fi
|
|
sleep "$IDLE_INTERVAL_SECONDS"
|
|
waited=$((waited + IDLE_INTERVAL_SECONDS))
|
|
done
|
|
echo "instance $was did not end itself within ${IDLE_TIMEOUT_SECONDS}s — stopping it; its running build is re-enqueued by the recovery" >&2
|
|
return 0
|
|
}
|
|
|
|
# Runs as the transient unit, once the build that staged the runtime has ended:
|
|
# wait for the instance to go idle, stop it, swap, start, and put the previous
|
|
# runtime back when the new one does not answer. Everything it prints lands in the
|
|
# journal of that unit. The swap happens here, between stop and start, so the JVM
|
|
# being replaced never sees its own runtime change underneath it.
|
|
restart_and_verify() {
|
|
local unit="$1" install_dir="$2" port="$3" self_restart_pid="${4:-}"
|
|
ensure_user_bus
|
|
if [ -n "$self_restart_pid" ]; then
|
|
await_instance_exit "$unit" "$self_restart_pid"
|
|
else
|
|
await_idle "$port"
|
|
fi
|
|
# also after the instance ended itself: `Restart=always` has the old runtime
|
|
# queued to come back, and swapping under it is what PR#64 was about
|
|
echo "stopping $unit to swap in the staged runtime"
|
|
systemctl --user stop "$unit" || echo "systemctl stop reported a failure; swapping anyway" >&2
|
|
swap_staged "$install_dir"
|
|
echo "starting $unit into the newly installed runtime"
|
|
# a failed start command must not skip the health check: whether the
|
|
# instance answers is what decides, not what systemctl reported
|
|
systemctl --user start "$unit" || echo "systemctl start reported a failure; checking the instance anyway" >&2
|
|
if await_instance "$port"; then
|
|
echo "the new runtime answers on port $port"
|
|
return 0
|
|
fi
|
|
echo "the new runtime did not answer on port $port within ${HEALTH_TIMEOUT_SECONDS}s — rolling back to werkator.prev" >&2
|
|
if [ ! -d "$install_dir/werkator.prev" ]; then
|
|
echo "no werkator.prev to roll back to; leaving the installation as it is" >&2
|
|
return 1
|
|
fi
|
|
rm -rf "$install_dir/werkator.failed"
|
|
mv "$install_dir/werkator" "$install_dir/werkator.failed"
|
|
mv "$install_dir/werkator.prev" "$install_dir/werkator"
|
|
systemctl --user restart "$unit"
|
|
if await_instance "$port"; then
|
|
echo "rolled back; the previous runtime answers again (the failed one is werkator.failed)" >&2
|
|
else
|
|
echo "rolled back, but the previous runtime does not answer either — manual intervention needed" >&2
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Schedules the restart to happen after this build has ended. The transient unit is
|
|
# started by the user manager, so stopping the service does not end it — which is
|
|
# the whole point: a restart triggered from inside the build would kill the build.
|
|
schedule_restart() {
|
|
local unit="$1" install_dir="$2" port="$3" watchdog restart_unit
|
|
local -a self_restart=()
|
|
watchdog="$install_dir/bin/$WATCHDOG_NAME"
|
|
restart_unit="$(restart_unit_for "$unit")"
|
|
# read here, while the instance that runs this build is still the running one
|
|
if [ "${WERKATOR_RESTARTS_INSTANCE:-}" = "true" ]; then
|
|
self_restart=(--self-restart-pid "$(main_pid_of "$unit")")
|
|
fi
|
|
mkdir -p "$install_dir/bin"
|
|
# from the installation, not from the worktree: the worktree is reused by the
|
|
# next build of this branch and may hold another commit by the time this runs
|
|
install -m 755 "$(readlink -f "${BASH_SOURCE[0]}")" "$watchdog"
|
|
systemctl --user stop "$restart_unit.timer" "$restart_unit.service" >/dev/null 2>&1 || true
|
|
if [ "${#self_restart[@]}" -gt 0 ]; then
|
|
echo "==> This instance ends itself after this build; the transient unit $restart_unit swaps and starts the new version"
|
|
else
|
|
echo "==> Scheduling the restart in ${RESTART_DELAY_SECONDS}s (transient unit $restart_unit)"
|
|
fi
|
|
systemd-run --user --collect --quiet \
|
|
--on-active="$RESTART_DELAY_SECONDS" \
|
|
--unit="$restart_unit" \
|
|
"$watchdog" --restart-and-verify --unit "$unit" --install-dir "$install_dir" --port "$port" \
|
|
${self_restart[@]+"${self_restart[@]}"}
|
|
echo " journalctl --user -u $restart_unit -f follows the restart and its health check"
|
|
}
|
|
|
|
main() {
|
|
local mode="deploy" install_dir="" bundle="" unit="" port="" self_restart_pid=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--no-restart) mode="swap-only"; shift ;;
|
|
--restart-and-verify) mode="watchdog"; shift ;;
|
|
--self-restart-pid) self_restart_pid="${2:?missing value for --self-restart-pid}"; shift 2 ;;
|
|
--install-dir) install_dir="${2:?missing value for --install-dir}"; shift 2 ;;
|
|
--bundle) bundle="${2:?missing value for --bundle}"; shift 2 ;;
|
|
--unit) unit="${2:?missing value for --unit}"; shift 2 ;;
|
|
--port) port="${2:?missing value for --port}"; shift 2 ;;
|
|
-h|--help) usage ;;
|
|
*) die "unknown argument: $1 (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
if [ "$mode" = "watchdog" ]; then
|
|
[ -n "$unit" ] && [ -n "$install_dir" ] && [ -n "$port" ] ||
|
|
die "--restart-and-verify needs --unit, --install-dir and --port"
|
|
restart_and_verify "$unit" "$install_dir" "$port" "$self_restart_pid"
|
|
return
|
|
fi
|
|
|
|
# Called by tools/remote with both of them (and --no-restart, which says at the
|
|
# call site what this path does anyway): no git worktree, no systemd, no running
|
|
# instance to ask — just the staged swap, the part the two must not implement twice.
|
|
if [ -n "$install_dir" ] && [ -n "$bundle" ]; then
|
|
[ -e "$bundle" ] || die "runtime bundle not found: $bundle"
|
|
[ -d "$install_dir" ] || die "installation directory not found: $install_dir"
|
|
stage_and_verify "$install_dir" "$bundle"
|
|
swap_staged "$install_dir"
|
|
return
|
|
fi
|
|
|
|
local repo_dir
|
|
require_linked_worktree
|
|
repo_dir="$(primary_checkout)"
|
|
local worktree
|
|
worktree="$(git rev-parse --show-toplevel)"
|
|
# the directory first: the preceding build produced it here, and a bundle built
|
|
# on this host has no reason to be packed and unpacked again
|
|
if [ -z "$bundle" ]; then
|
|
if [ -d "$worktree/$BUNDLE_DIR_RELATIVE_PATH" ]; then
|
|
bundle="$worktree/$BUNDLE_DIR_RELATIVE_PATH"
|
|
else
|
|
bundle="$worktree/$BUNDLE_TARBALL_RELATIVE_PATH"
|
|
fi
|
|
fi
|
|
# A restart of `deploy` alone runs without its predecessor, so the worktree
|
|
# holds no bundle. Installing whatever happens to lie around would deploy an
|
|
# older version than the one the History says was deployed.
|
|
[ -e "$bundle" ] ||
|
|
die "no runtime bundle at $worktree/$BUNDLE_DIR_RELATIVE_PATH — the deploy build runs after a green build that built it; restart that one"
|
|
|
|
ensure_user_bus
|
|
# the watchdog decides by asking the instance; without curl it would find every
|
|
# deployment broken and roll back a perfectly good one
|
|
command -v curl >/dev/null 2>&1 ||
|
|
die "no curl on this host — the restart watchdog needs it to tell a started instance from a broken one"
|
|
[ -n "$unit" ] || unit="$(unit_name_for "$repo_dir")"
|
|
local working_dir
|
|
working_dir="$(systemctl --user show -p WorkingDirectory --value "$unit" 2>/dev/null || true)"
|
|
# compared resolved: a webspace reaches the same directory under more than one
|
|
# path, and a symlink is no reason to refuse the deployment
|
|
[ -z "$working_dir" ] || [ "$(readlink -f "$working_dir")" = "$(readlink -f "$repo_dir")" ] ||
|
|
die "$unit watches $working_dir, but this build belongs to $repo_dir — refusing to deploy into a foreign instance"
|
|
[ -n "$install_dir" ] || install_dir="$(install_dir_from_unit "$unit")"
|
|
echo "==> Deploying into $install_dir (unit $unit, repository $repo_dir)"
|
|
|
|
port="$(port_of_instance "$install_dir" "$repo_dir")"
|
|
stage_and_verify "$install_dir" "$bundle"
|
|
|
|
if [ "$mode" = "swap-only" ]; then
|
|
# the caller stops and starts the service itself, so it owns the moment
|
|
swap_staged "$install_dir"
|
|
echo "==> Swapped; no restart scheduled (--no-restart)"
|
|
return
|
|
fi
|
|
# staged and proven, installed by nobody yet: the transient unit swaps it in
|
|
# while the service is down, so the running JVM keeps its own runtime
|
|
schedule_restart "$unit" "$install_dir" "$port"
|
|
}
|
|
|
|
main "$@"
|