signal-bot/scripts/reload.sh
Krzysztof kuhy Rudnicki 0717df646a
Some checks are pending
CI / check (push) Waiting to run
CI / web (push) Waiting to run
Rebuild the command table on demand, and survive a tag whose file is gone
The registry used to be built once, inline in build_bot, so every change to
the data behind it -- a person tagged in the web UI, an alias, soon a command
switched off from the management page -- meant restarting the unit. That is
cheap and lossless, but it is a deploy step somebody has to remember, and a
21:37 report can land in the middle of it.

Now the registration sequence lives in wiring.build_registry, and
RegistryHolder runs it again on POST /_reload: the new table is built whole
and swapped in with one assignment, never patched in place, so a duplicate
trigger is found before anything the dispatcher reads has changed. Bad data
(ConfigError, DuplicateTriggerError) keeps the previous table and answers
409 with the reason; a programming error still surfaces as a 500.
scripts/reload.sh wraps it, with --code for the restart a Python change
needs, and an optional command to type in testbot so "reloaded" means
"answered". No Python hot-reload on purpose.

The loader change is what makes deploying this safe: two rows in the live
tags.json name files that are no longer in media/, and load_pack refused
them, so the next restart would have looped on ConfigError. A stale tag is
drift in a file a page edits mid-session, not a broken album -- it is now
skipped with a warning naming it. A tag that reaches outside the album is
still refused. pack_load.py split at the 250-line cap; pack_tags.py is the
half whose checks are not all fatal.

Proven offline in scripts/e2e.sh: !delta is unknown, a fifth person is
tagged into the synthetic pack, scripts/reload.sh runs, and the same
process posts delta's photo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 10:14:39 +02:00

122 lines
3.4 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Make the running bot pick up a change, and prove that it did.
#
# Two kinds of change, two mechanisms:
#
# scripts/reload.sh [cmd] data changed (aliases, an album, a
# command switched off): POST /_reload,
# the table is rebuilt in place, nothing
# restarts and no frame is lost.
# scripts/reload.sh --code [cmd] code changed: restart the unit, wait for
# /healthz to go green again. Lossless too
# (signal-cli replays what arrived), but
# a deploy step -- so it is the flag, not
# the default.
#
# With [cmd], the bot then types that command at itself in the test group,
# so "reloaded" means "answered", not "returned 200". E.g.
#
# scripts/reload.sh '!nowaosoba'
# scripts/reload.sh --code '!pomoc'
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly BOT="http://127.0.0.1:${BIND_PORT:-8137}"
readonly UNIT="signal-bot"
readonly TEST_GROUP="${RELOAD_GROUP:-testbot}"
RESTART=0
PROBE=""
usage() {
echo "Usage: $SCRIPT_NAME [--code] [command-to-selftest]"
echo " --code restart the unit (code changed) instead of rebuilding the table"
exit 0
}
validate_requirements() {
if ! command -v jq >/dev/null; then
echo "Error: jq is required" >&2
exit 1
fi
}
wait_healthy() {
local attempt=0
# 150 x 0.4 s: a restart is ~1 s, but signal-cli can take a while to
# accept the websocket again after its own reconnect.
until [[ "$(curl -s "${BOT}/healthz" | jq -r '.status // "down"')" == "ok" ]]; do
attempt=$((attempt + 1))
if [[ $attempt -ge 150 ]]; then
echo "Error: ${BOT}/healthz never reported ok" >&2
curl -s "${BOT}/healthz" >&2 || true
exit 1
fi
sleep 0.4
done
}
reload_data() {
local reply status
reply="$(curl -s -X POST "${BOT}/_reload")"
status="$(jq -r '.status // "?"' <<<"$reply")"
case "$status" in
reloaded)
echo "reloaded: $(jq -r '.commands' <<<"$reply") commands live"
;;
kept)
# The old table is still answering; the new data is what is broken.
echo "Error: reload refused, previous table kept: $(jq -r '.reason' <<<"$reply")" >&2
exit 1
;;
*)
echo "Error: unexpected answer from ${BOT}/_reload: ${reply}" >&2
exit 1
;;
esac
}
restart_code() {
systemctl --user restart "$UNIT"
wait_healthy
echo "restarted: $(curl -s "${BOT}/healthz" | jq -r '"\(.version) \(.pope)"')"
}
main() {
validate_requirements
if [[ $RESTART -eq 1 ]]; then
restart_code
else
wait_healthy
reload_data
fi
if [[ -n "$PROBE" ]]; then
"${SCRIPT_DIR}/selftest.sh" "$TEST_GROUP" "$PROBE"
fi
}
while [[ $# -gt 0 ]]; do
case $1 in
--code)
RESTART=1
shift
;;
-h | --help)
usage
;;
*)
PROBE="$1"
shift
;;
esac
done
main