0726ce3794
The AI could only see three static facts about the server, so it answered "quem tá online?" and "tá chovendo?" by insisting it had no access — true of the model, but not of the plugin, which has all of it on hand. It also had no tone: correct answers delivered like a manual, on a server whose whole point is people ribbing each other. Persona: five tones (zoeiro, amigao, seco, aldeao, neutro), switchable live with /ia personalidade <nome>. Personality is expressed only as extra system instructions and changes how the model talks, never what it may do. Every persona — including the blank one — carries Persona.GUARD, which restates the no-commands/no-server-access limits inside the persona's own frame, so a roleplay instruction cannot read as licence to claim powers the plugin does not grant it. The guard also forbids inventing stats, which matters now that real numbers are being fed in. The teasing personas each state where the line is; a test asserts every one of them does. ChatLog: a 50-line in-memory ring of public chat, the last few lines handed to the model so a follow-up like "quem tá reclamando aí?" has a referent. Written from the chat event (off the main thread) and read from /ia, so it is synchronised; a concurrency test hammers it from eight threads, because an unsynchronised deque here would throw ConcurrentModification into a player's answer. Recorded at MONITOR priority so what is stored is what the room saw — a zoacao swap included — and cancelled messages are never stored. Nothing touches disk. ServerState: who is online with their platform, dimension, time of day, weather, and the asker's coordinates, health, hunger and XP. Captured on the main thread before the async call — every field reads the Bukkit world API, which is not safe off it — and only the formatted string crosses the thread boundary. Formatting is pure and tested, including the negative-tick case a raw modulo would drop through every band. Styling: Java players get a hover card with the original question and the active persona, plus a click that pre-fills "/ia " for a follow-up. suggestCommand, not runCommand: nothing executes without the player pressing enter. Bedrock renders neither hover nor click, so it keeps the plain line, built through broadcastPerPlatform like every other interactive message here. preflight.sh: a read-only pre-restart harness. It verifies the jar opens, that plugin.yml declares all 16 commands, that the staged jar's hash matches the local build and is owned 1000:0, that the live config parses as YAML and carries the keys this deploy depends on, and that a rollback jar and config backup both exist. It never restarts anything. A missing YAML parser reports as "not checked" rather than "invalid" — a harness that cries wolf gets ignored. 176 tests, up from 101. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
196 lines
7.5 KiB
Bash
Executable File
196 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Pre-flight harness for a Canalhandia deploy.
|
|
#
|
|
# Every check below is READ-ONLY. It never restarts the server, never writes to
|
|
# the plugins directory, and never touches the world. Run it after staging a
|
|
# jar and before asking Crafty to restart: a red line here is a crash-on-boot
|
|
# you get to fix while the server is still up.
|
|
#
|
|
# Usage: ./preflight.sh [path/to/new.jar]
|
|
# (defaults to target/Canalhandia-1.0.0.jar)
|
|
|
|
set -uo pipefail
|
|
|
|
JAR="${1:-target/Canalhandia-1.0.0.jar}"
|
|
NS=minecraft
|
|
SERVER_ID=6e39a8b2-300b-42d6-8139-f397c23e461b
|
|
PLUGINS="/crafty/servers/${SERVER_ID}/plugins"
|
|
K="microk8s kubectl -n ${NS}"
|
|
|
|
fail=0
|
|
pass() { printf ' \033[32mOK\033[0m %s\n' "$1"; }
|
|
warn() { printf ' \033[33mWARN\033[0m %s\n' "$1"; }
|
|
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail + 1)); }
|
|
head() { printf '\n\033[1m%s\033[0m\n' "$1"; }
|
|
|
|
CRAFTY=$($K get pods -l app=crafty-controller -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
|
|
if [ -z "$CRAFTY" ]; then
|
|
CRAFTY=$($K get pods --no-headers 2>/dev/null | awk '/^crafty-controller/ {print $1; exit}')
|
|
fi
|
|
|
|
head "1. Local build"
|
|
|
|
if [ ! -f "$JAR" ]; then
|
|
bad "jar not found: $JAR"
|
|
else
|
|
pass "jar present: $JAR ($(stat -c%s "$JAR") bytes)"
|
|
# A jar that does not open is a jar the server will refuse at boot.
|
|
if unzip -t "$JAR" >/dev/null 2>&1; then
|
|
pass "jar archive is intact"
|
|
else
|
|
bad "jar archive is corrupt (unzip -t failed)"
|
|
fi
|
|
# plugin.yml is the only file Bukkit truly requires; without it the plugin is
|
|
# skipped silently and nothing in the deploy takes effect.
|
|
if unzip -p "$JAR" plugin.yml >/dev/null 2>&1; then
|
|
pass "plugin.yml present in jar"
|
|
else
|
|
bad "plugin.yml MISSING from jar — the plugin would not load at all"
|
|
fi
|
|
# Every command the code registers must exist in plugin.yml, or register()
|
|
# logs a warning and the command silently does nothing in game.
|
|
missing=""
|
|
for cmd in canalhandia curiosidade adivinha enquete ranking reagir reacoes \
|
|
palpite votar legal wow top f ia iap errado; do
|
|
unzip -p "$JAR" plugin.yml 2>/dev/null | grep -qE "^ ${cmd}:" || missing="$missing $cmd"
|
|
done
|
|
if [ -z "$missing" ]; then
|
|
pass "all 16 commands declared in plugin.yml"
|
|
else
|
|
bad "commands missing from plugin.yml:$missing"
|
|
fi
|
|
# config.yml ships defaults; a jar without it means saveDefaultConfig() writes
|
|
# nothing and every setting silently falls back to the hardcoded default.
|
|
if unzip -p "$JAR" config.yml >/dev/null 2>&1; then
|
|
pass "config.yml present in jar"
|
|
else
|
|
bad "config.yml MISSING from jar"
|
|
fi
|
|
fi
|
|
|
|
head "2. Tests"
|
|
if [ -d target/surefire-reports ]; then
|
|
tests=$(grep -ho 'tests="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null |
|
|
grep -o '[0-9]*' | paste -sd+ | bc)
|
|
bads=$(grep -ho 'failures="[0-9]*"\|errors="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null |
|
|
grep -o '[0-9]*' | paste -sd+ | bc)
|
|
if [ "${bads:-1}" = "0" ]; then
|
|
pass "${tests} tests, 0 failures/errors"
|
|
else
|
|
bad "${bads} test failures/errors — run mvn test"
|
|
fi
|
|
else
|
|
warn "no surefire reports; run the test suite before deploying"
|
|
fi
|
|
|
|
head "3. Cluster"
|
|
if [ -z "$CRAFTY" ]; then
|
|
bad "crafty-controller pod not found in namespace ${NS}"
|
|
else
|
|
pass "crafty pod: $CRAFTY"
|
|
if $K exec "$CRAFTY" -- test -d "$PLUGINS" 2>/dev/null; then
|
|
pass "plugins directory reachable"
|
|
else
|
|
bad "cannot reach $PLUGINS"
|
|
fi
|
|
fi
|
|
|
|
head "4. Staged jar on the server"
|
|
if [ -n "$CRAFTY" ] && [ -f "$JAR" ]; then
|
|
local_sum=$(sha256sum "$JAR" | cut -c1-8)
|
|
remote_sum=$($K exec "$CRAFTY" -- sha256sum "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null | cut -c1-8)
|
|
if [ -z "$remote_sum" ]; then
|
|
bad "no Canalhandia jar staged on the server"
|
|
elif [ "$local_sum" = "$remote_sum" ]; then
|
|
pass "staged jar matches the local build (sha $local_sum)"
|
|
else
|
|
bad "staged jar is sha $remote_sum, local build is $local_sum — copy it again"
|
|
fi
|
|
# Ownership: the JVM runs as uid 1000 / gid 0. A root-owned jar is a jar the
|
|
# server cannot read, and the failure looks like "plugin just did not load".
|
|
owner=$($K exec "$CRAFTY" -- stat -c '%u:%g' "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null)
|
|
if [ "$owner" = "1000:0" ]; then
|
|
pass "jar ownership 1000:0"
|
|
else
|
|
bad "jar ownership is '$owner', expected 1000:0 — chown it"
|
|
fi
|
|
# A rollback target must exist before, not after, something goes wrong.
|
|
if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia-1.0.0.jar.bak-* >/dev/null 2>&1"; then
|
|
pass "rollback jar(s) present"
|
|
else
|
|
bad "no .bak jar to roll back to"
|
|
fi
|
|
fi
|
|
|
|
head "5. Live config"
|
|
if [ -n "$CRAFTY" ]; then
|
|
cfg="$PLUGINS/Canalhandia/config.yml"
|
|
if $K exec "$CRAFTY" -- test -f "$cfg" 2>/dev/null; then
|
|
pass "config.yml present on the server"
|
|
# Parsed HERE rather than in the pod: the crafty image has no python, and a
|
|
# config.yml that does not parse is a plugin that disables itself on boot.
|
|
tmp=$(mktemp)
|
|
if $K exec "$CRAFTY" -- cat "$cfg" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
|
# Pick a parser. "no parser available" must NOT be reported as "invalid":
|
|
# a harness that cries wolf is a harness people learn to ignore.
|
|
if python3 -c "import yaml" 2>/dev/null; then
|
|
yaml_check() { python3 -c "import yaml,sys;yaml.safe_load(open(sys.argv[1]))" "$1"; }
|
|
elif command -v docker >/dev/null 2>&1; then
|
|
yaml_check() { docker run --rm -v "$1":/c.yml:ro python:3.12-slim \
|
|
sh -c "pip install -q pyyaml >/dev/null 2>&1 && python3 -c \
|
|
'import yaml;yaml.safe_load(open(\"/c.yml\"))'"; }
|
|
else
|
|
yaml_check() { return 2; }
|
|
fi
|
|
yaml_check "$tmp" 2>/dev/null
|
|
case $? in
|
|
0) pass "config.yml parses as valid YAML" ;;
|
|
2) warn "no YAML parser available (pip install pyyaml) — not checked" ;;
|
|
*) bad "config.yml on the server is NOT valid YAML — the plugin would fail to load" ;;
|
|
esac
|
|
# The keys this deploy depends on. A missing key is not fatal (Settings
|
|
# has defaults) but it means the merge did not happen as intended.
|
|
for key in "modulos:" "zoacao:" "personalidade:" "contexto-chat:" "estado-servidor:"; do
|
|
if grep -q "$key" "$tmp"; then
|
|
pass "config has ${key%:}"
|
|
else
|
|
warn "config has no '${key%:}' — will fall back to the built-in default"
|
|
fi
|
|
done
|
|
else
|
|
warn "could not read config.yml out of the pod"
|
|
fi
|
|
rm -f "$tmp"
|
|
if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia/config.yml.bak-* >/dev/null 2>&1"; then
|
|
pass "config backup(s) present"
|
|
else
|
|
bad "no config.yml.bak-* to roll back to"
|
|
fi
|
|
else
|
|
bad "config.yml missing on the server"
|
|
fi
|
|
fi
|
|
|
|
head "6. Server health right now"
|
|
if [ -n "$CRAFTY" ]; then
|
|
logf="/crafty/servers/${SERVER_ID}/logs/latest.log"
|
|
# tr -dc keeps digits only: grep -c prints nothing on no-match under some
|
|
# shells, and the stray newline made the numeric test below explode.
|
|
online=$($K exec "$CRAFTY" -- sh -c "grep -c 'joined the game' $logf" 2>/dev/null | tr -dc '0-9')
|
|
pass "log readable (${online:-0} join lines this session)"
|
|
errs=$($K exec "$CRAFTY" -- sh -c "tail -500 $logf | grep -ci 'ERROR\]'" 2>/dev/null | tr -dc '0-9')
|
|
if [ "${errs:-0}" -gt 0 ]; then
|
|
warn "${errs} ERROR lines in the last 500 — read them before restarting"
|
|
else
|
|
pass "no ERROR lines in the last 500"
|
|
fi
|
|
fi
|
|
|
|
printf '\n'
|
|
if [ "$fail" -eq 0 ]; then
|
|
printf '\033[32mPRE-FLIGHT CLEAN — safe to restart.\033[0m\n'
|
|
exit 0
|
|
fi
|
|
printf '\033[31m%d CHECK(S) FAILED — do NOT restart yet.\033[0m\n' "$fail"
|
|
exit 1
|