From 7fec9aecf79998e789a2190c723cb3464da5f853 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:51:59 +0000 Subject: [PATCH 01/21] Initial plan From 2071551f07cbcbdafa5d4b31382f552ef8449a7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:57:16 +0000 Subject: [PATCH 02/21] feat: add HTTP healthcheck endpoint for bot runtime checks Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/c97c5f48-bf46-4233-9bd6-a5b2e3b9fe04 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- .env.example | 5 +- Dockerfile | 2 + README.md | 17 +++++- src/__main__.py | 25 ++++++++ src/config.py | 7 +++ src/healthcheck.py | 139 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/healthcheck.py diff --git a/.env.example b/.env.example index 1967b3e..1e7441a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -TOKEN="YOUR BOT TOKEN HERE" \ No newline at end of file +TOKEN="YOUR BOT TOKEN HERE" +HEALTHCHECK_HOST="127.0.0.1" +HEALTHCHECK_PORT="8080" +HEALTHCHECK_PATH="/" diff --git a/Dockerfile b/Dockerfile index 839ee5c..48e520c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,6 @@ RUN pip install -r requirements.txt COPY src/ ./src USER appuser +EXPOSE 8080 + CMD ["python", "-m", "src"] diff --git a/README.md b/README.md index 2f214e8..f94f668 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,22 @@ Versa is a simple utility Discord bot, with the main goal of being open source a #### ⚠ **Further support with self-hosting will not be provided.** ⚠ +### Healthcheck endpoint + +The bot exposes an HTTP healthcheck endpoint for deployment platforms (such as Coolify). + +- Method/path: `GET /` (configurable via `HEALTHCHECK_PATH`) +- Default bind: `127.0.0.1:8080` +- Config via env vars: + - `HEALTHCHECK_HOST` + - `HEALTHCHECK_PORT` + - `HEALTHCHECK_PATH` + +The endpoint returns: +- `200` when DB is responsive, Discord is connected, and no global Discord rate-limit is active +- `503` when any check is failing + # License This project is licensed under AGPL-3.0. Forks and redistributions must remain open-source. See the LICENSE file for -further info \ No newline at end of file +further info diff --git a/src/__main__.py b/src/__main__.py index b02a597..927180a 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -4,7 +4,9 @@ import discord from src import log_setup +from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT from src.database import init_db, shutdown_db +from src.healthcheck import HealthcheckServer from .config import TOKEN @@ -33,13 +35,26 @@ async def on_ready() -> None: async def start() -> None: original_exc = None + healthcheck_server = HealthcheckServer( + bot, + host=HEALTHCHECK_HOST, + port=HEALTHCHECK_PORT, + path=HEALTHCHECK_PATH, + ) try: await init_db() + await healthcheck_server.start() async with bot: await bot.start(TOKEN) except Exception as e: # noqa: BLE001 original_exc = e finally: + healthcheck_stop_exc = None + try: + await healthcheck_server.stop() + except Exception as e3: # noqa: BLE001 + healthcheck_stop_exc = e3 + try: await shutdown_db() except Exception as e2: @@ -47,7 +62,17 @@ async def start() -> None: msg = "Multiple errors happened when starting the bot" raise ExceptionGroup(msg, [original_exc, e2]) from None + if healthcheck_stop_exc: + msg = "Multiple errors happened during shutdown" + + raise ExceptionGroup(msg, [healthcheck_stop_exc, e2]) from None raise + if healthcheck_stop_exc: + if original_exc: + msg = "Multiple errors happened when starting the bot" + + raise ExceptionGroup(msg, [original_exc, healthcheck_stop_exc]) from None + raise healthcheck_stop_exc if original_exc: raise original_exc diff --git a/src/config.py b/src/config.py index c96d8ab..0be289d 100644 --- a/src/config.py +++ b/src/config.py @@ -9,3 +9,10 @@ DB_PATH = Path(os.getenv("DB_PATH") or Path("data/database.db")).absolute() DB_PATH.parent.mkdir(parents=True, exist_ok=True) + +HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST", "127.0.0.1") +HEALTHCHECK_PORT = int(os.getenv("HEALTHCHECK_PORT", "8080")) +HEALTHCHECK_PATH = os.getenv("HEALTHCHECK_PATH", "/") + +if not HEALTHCHECK_PATH.startswith("/"): + HEALTHCHECK_PATH = f"/{HEALTHCHECK_PATH}" diff --git a/src/healthcheck.py b/src/healthcheck.py new file mode 100644 index 0000000..4849f6a --- /dev/null +++ b/src/healthcheck.py @@ -0,0 +1,139 @@ +import asyncio +import json +import logging +from contextlib import suppress +from typing import Any + +import discord +from tortoise import Tortoise +from tortoise.exceptions import ConfigurationError, DBConnectionError, OperationalError + +logger = logging.getLogger(__name__) + +_REQUEST_TIMEOUT_SECONDS = 5 + + +class HealthcheckServer: + def __init__( + self, + bot: discord.Bot, + *, + host: str, + port: int, + path: str = "/", + ) -> None: + self.bot = bot + self.host = host + self.port = port + self.path = path + self._server: asyncio.AbstractServer | None = None + + async def start(self) -> None: + self._server = await asyncio.start_server(self._handle_connection, host=self.host, port=self.port) + logger.info("Healthcheck server listening on http://%s:%s%s", self.host, self.port, self.path) + + async def stop(self) -> None: + if self._server is None: + return + + self._server.close() + await self._server.wait_closed() + self._server = None + + async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + response_status = 500 + response_body: dict[str, Any] = {"status": "error"} + + try: + request_line = await asyncio.wait_for(reader.readline(), timeout=_REQUEST_TIMEOUT_SECONDS) + if not request_line: + return + + method, raw_target, _ = request_line.decode("utf-8", errors="replace").strip().split(maxsplit=2) + await self._consume_headers(reader) + + target = raw_target.split("?", maxsplit=1)[0] + if method != "GET": + response_status = 405 + response_body = {"status": "error", "reason": "method_not_allowed"} + elif target != self.path: + response_status = 404 + response_body = {"status": "error", "reason": "not_found"} + else: + response_status, response_body = await self._health_response() + except (UnicodeDecodeError, ValueError): + response_status = 400 + response_body = {"status": "error", "reason": "bad_request"} + except TimeoutError: + response_status = 408 + response_body = {"status": "error", "reason": "request_timeout"} + finally: + writer.write(self._build_response(response_status, response_body)) + with suppress(ConnectionError): + await writer.drain() + + writer.close() + with suppress(ConnectionError): + await writer.wait_closed() + + async def _health_response(self) -> tuple[int, dict[str, Any]]: + db_connected = await self._is_db_connected() + discord_connected = self._is_discord_connected() + discord_unrate_limited = self._is_discord_unrate_limited() + + checks = { + "database_connected": db_connected, + "discord_connected": discord_connected, + "discord_no_global_ratelimit": discord_unrate_limited, + } + healthy = all(checks.values()) + return ( + 200 if healthy else 503, + { + "status": "ok" if healthy else "degraded", + "checks": checks, + }, + ) + + async def _is_db_connected(self) -> bool: + try: + connection = Tortoise.get_connection("default") + await connection.execute_query("SELECT 1") + except (ConfigurationError, DBConnectionError, OperationalError): + return False + return True + + def _is_discord_connected(self) -> bool: + return self.bot.is_ready() and not self.bot.is_closed() + + def _is_discord_unrate_limited(self) -> bool: + global_rate_limit_gate = getattr(self.bot.http, "_global_over", None) + if isinstance(global_rate_limit_gate, asyncio.Event): + return global_rate_limit_gate.is_set() + return not self.bot.is_ws_ratelimited() + + async def _consume_headers(self, reader: asyncio.StreamReader) -> None: + while True: + line = await asyncio.wait_for(reader.readline(), timeout=_REQUEST_TIMEOUT_SECONDS) + if not line or line in {b"\r\n", b"\n"}: + return + + def _build_response(self, status_code: int, body: dict[str, Any]) -> bytes: + status_text = { + 200: "OK", + 400: "Bad Request", + 404: "Not Found", + 405: "Method Not Allowed", + 408: "Request Timeout", + 500: "Internal Server Error", + 503: "Service Unavailable", + }.get(status_code, "Internal Server Error") + payload = json.dumps(body, separators=(",", ":"), sort_keys=True).encode() + headers = ( + f"HTTP/1.1 {status_code} {status_text}\r\n" + "Content-Type: application/json\r\n" + f"Content-Length: {len(payload)}\r\n" + "Connection: close\r\n" + "\r\n" + ).encode() + return headers + payload From 314661529fda5f4e81699f5e830c9443b0b7f1db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:59:35 +0000 Subject: [PATCH 03/21] chore: polish healthcheck shutdown exception naming Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/c97c5f48-bf46-4233-9bd6-a5b2e3b9fe04 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index 927180a..343d1cc 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -52,8 +52,8 @@ async def start() -> None: healthcheck_stop_exc = None try: await healthcheck_server.stop() - except Exception as e3: # noqa: BLE001 - healthcheck_stop_exc = e3 + except Exception as healthcheck_exc: # noqa: BLE001 + healthcheck_stop_exc = healthcheck_exc try: await shutdown_db() From 7543163ddc91d846b90a8637b72cf5530adc2f8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:25:42 +0000 Subject: [PATCH 04/21] Address PR feedback on healthcheck imports and probe checks Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/9e556274-2b93-48f7-9e72-478e586eae74 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- README.md | 2 +- src/__main__.py | 4 +--- src/healthcheck.py | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f94f668..7db7ca1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The bot exposes an HTTP healthcheck endpoint for deployment platforms (such as C - `HEALTHCHECK_PATH` The endpoint returns: -- `200` when DB is responsive, Discord is connected, and no global Discord rate-limit is active +- `200` when DB is responsive, Discord is connected, Discord shard heartbeat latency is healthy, and no global Discord rate-limit is active - `503` when any check is failing # License diff --git a/src/__main__.py b/src/__main__.py index 343d1cc..c6b6129 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -3,13 +3,11 @@ import discord +from config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN from src import log_setup -from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT from src.database import init_db, shutdown_db from src.healthcheck import HealthcheckServer -from .config import TOKEN - log_setup.setup_logging(logging.INFO) logger = logging.getLogger(__name__) diff --git a/src/healthcheck.py b/src/healthcheck.py index 4849f6a..25b13dd 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import math from contextlib import suppress from typing import Any @@ -11,6 +12,7 @@ logger = logging.getLogger(__name__) _REQUEST_TIMEOUT_SECONDS = 5 +_MAX_HEARTBEAT_LATENCY_SECONDS = 60 class HealthcheckServer: @@ -80,11 +82,13 @@ async def _health_response(self) -> tuple[int, dict[str, Any]]: db_connected = await self._is_db_connected() discord_connected = self._is_discord_connected() discord_unrate_limited = self._is_discord_unrate_limited() + discord_heartbeat_ok = self._is_discord_heartbeat_healthy() checks = { "database_connected": db_connected, "discord_connected": discord_connected, "discord_no_global_ratelimit": discord_unrate_limited, + "discord_heartbeats_healthy": discord_heartbeat_ok, } healthy = all(checks.values()) return ( @@ -107,18 +111,22 @@ def _is_discord_connected(self) -> bool: return self.bot.is_ready() and not self.bot.is_closed() def _is_discord_unrate_limited(self) -> bool: - global_rate_limit_gate = getattr(self.bot.http, "_global_over", None) - if isinstance(global_rate_limit_gate, asyncio.Event): - return global_rate_limit_gate.is_set() return not self.bot.is_ws_ratelimited() - async def _consume_headers(self, reader: asyncio.StreamReader) -> None: + def _is_discord_heartbeat_healthy(self) -> bool: + latencies = self.bot.latencies if isinstance(self.bot, discord.AutoShardedClient) else [(0, self.bot.latency)] + + return all(math.isfinite(latency) and latency <= _MAX_HEARTBEAT_LATENCY_SECONDS for _, latency in latencies) + + @staticmethod + async def _consume_headers(reader: asyncio.StreamReader) -> None: while True: line = await asyncio.wait_for(reader.readline(), timeout=_REQUEST_TIMEOUT_SECONDS) if not line or line in {b"\r\n", b"\n"}: return - def _build_response(self, status_code: int, body: dict[str, Any]) -> bytes: + @staticmethod + def _build_response(status_code: int, body: dict[str, Any]) -> bytes: status_text = { 200: "OK", 400: "Bad Request", From 0823976655a1125663818d1972c0ff04b1edf1e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:30:07 +0000 Subject: [PATCH 05/21] Refine healthcheck probes per PR feedback Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/9e556274-2b93-48f7-9e72-478e586eae74 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/__main__.py | 6 +++++- src/healthcheck.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index c6b6129..a8e661f 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -3,7 +3,11 @@ import discord -from config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN +try: + from config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN +except ImportError: # pragma: no cover - runtime import fallback + from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN + from src import log_setup from src.database import init_db, shutdown_db from src.healthcheck import HealthcheckServer diff --git a/src/healthcheck.py b/src/healthcheck.py index 25b13dd..c590195 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -114,9 +114,13 @@ def _is_discord_unrate_limited(self) -> bool: return not self.bot.is_ws_ratelimited() def _is_discord_heartbeat_healthy(self) -> bool: - latencies = self.bot.latencies if isinstance(self.bot, discord.AutoShardedClient) else [(0, self.bot.latency)] + if isinstance(self.bot, discord.AutoShardedClient): + return all( + math.isfinite(latency) and 0 <= latency <= _MAX_HEARTBEAT_LATENCY_SECONDS + for _, latency in self.bot.latencies + ) - return all(math.isfinite(latency) and latency <= _MAX_HEARTBEAT_LATENCY_SECONDS for _, latency in latencies) + return math.isfinite(self.bot.latency) and 0 <= self.bot.latency <= _MAX_HEARTBEAT_LATENCY_SECONDS @staticmethod async def _consume_headers(reader: asyncio.StreamReader) -> None: From e0ba7f938818c20be0b37810c0736df767c3027e Mon Sep 17 00:00:00 2001 From: ToothyDev Date: Tue, 14 Apr 2026 14:33:06 +0200 Subject: [PATCH 06/21] Simplify import --- src/__main__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index a8e661f..69c0bf1 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -3,12 +3,8 @@ import discord -try: - from config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN -except ImportError: # pragma: no cover - runtime import fallback - from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN - from src import log_setup +from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN from src.database import init_db, shutdown_db from src.healthcheck import HealthcheckServer From dd82e3960b7ddcc2f3004fa46452f744411d8c8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:42:37 +0000 Subject: [PATCH 07/21] Tighten heartbeat latency health threshold Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/03a42855-7f38-4d7c-a520-0218b63e29f2 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/healthcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/healthcheck.py b/src/healthcheck.py index c590195..6680f79 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) _REQUEST_TIMEOUT_SECONDS = 5 -_MAX_HEARTBEAT_LATENCY_SECONDS = 60 +_MAX_HEARTBEAT_LATENCY_SECONDS = 10 class HealthcheckServer: From 8bdd1db63fc978019bf925b23e6d48641dc18cbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:48:28 +0000 Subject: [PATCH 08/21] Add healthcheck module and method docstrings Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/ca4099e0-8c02-470a-a2fa-c94c5d819746 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/healthcheck.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/healthcheck.py b/src/healthcheck.py index 6680f79..801b3c4 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -1,3 +1,5 @@ +"""Minimal HTTP healthcheck server for deployment probes.""" + import asyncio import json import logging @@ -16,6 +18,8 @@ class HealthcheckServer: + """Serve HTTP healthcheck responses for runtime dependency status.""" + def __init__( self, bot: discord.Bot, @@ -24,6 +28,14 @@ def __init__( port: int, path: str = "/", ) -> None: + """ + Initialize the healthcheck server. + + :param bot: Discord bot instance used for runtime status checks. + :param host: Interface address for the healthcheck listener. + :param port: TCP port for the healthcheck listener. + :param path: HTTP path that serves health responses. + """ self.bot = bot self.host = host self.port = port @@ -31,10 +43,12 @@ def __init__( self._server: asyncio.AbstractServer | None = None async def start(self) -> None: + """Start listening for healthcheck HTTP requests.""" self._server = await asyncio.start_server(self._handle_connection, host=self.host, port=self.port) logger.info("Healthcheck server listening on http://%s:%s%s", self.host, self.port, self.path) async def stop(self) -> None: + """Stop the healthcheck listener if it is running.""" if self._server is None: return @@ -43,6 +57,12 @@ async def stop(self) -> None: self._server = None async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + """ + Process a single HTTP request and return a JSON response. + + :param reader: Stream reader for the client connection. + :param writer: Stream writer for the client connection. + """ response_status = 500 response_body: dict[str, Any] = {"status": "error"} @@ -79,6 +99,7 @@ async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio await writer.wait_closed() async def _health_response(self) -> tuple[int, dict[str, Any]]: + """Build the current health payload and matching HTTP status code.""" db_connected = await self._is_db_connected() discord_connected = self._is_discord_connected() discord_unrate_limited = self._is_discord_unrate_limited() @@ -100,6 +121,7 @@ async def _health_response(self) -> tuple[int, dict[str, Any]]: ) async def _is_db_connected(self) -> bool: + """Return whether the configured database connection is responsive.""" try: connection = Tortoise.get_connection("default") await connection.execute_query("SELECT 1") @@ -108,12 +130,15 @@ async def _is_db_connected(self) -> bool: return True def _is_discord_connected(self) -> bool: + """Return whether the Discord client is ready and not closed.""" return self.bot.is_ready() and not self.bot.is_closed() def _is_discord_unrate_limited(self) -> bool: + """Return whether the Discord websocket is not globally ratelimited.""" return not self.bot.is_ws_ratelimited() def _is_discord_heartbeat_healthy(self) -> bool: + """Return whether Discord heartbeat latencies are within the healthy threshold.""" if isinstance(self.bot, discord.AutoShardedClient): return all( math.isfinite(latency) and 0 <= latency <= _MAX_HEARTBEAT_LATENCY_SECONDS @@ -124,6 +149,11 @@ def _is_discord_heartbeat_healthy(self) -> bool: @staticmethod async def _consume_headers(reader: asyncio.StreamReader) -> None: + """ + Read request headers until an empty line is reached. + + :param reader: Stream reader for the client connection. + """ while True: line = await asyncio.wait_for(reader.readline(), timeout=_REQUEST_TIMEOUT_SECONDS) if not line or line in {b"\r\n", b"\n"}: @@ -131,6 +161,13 @@ async def _consume_headers(reader: asyncio.StreamReader) -> None: @staticmethod def _build_response(status_code: int, body: dict[str, Any]) -> bytes: + """ + Build a raw HTTP JSON response payload. + + :param status_code: HTTP status code to emit. + :param body: Response body content. + :returns: Serialized HTTP response bytes. + """ status_text = { 200: "OK", 400: "Bad Request", From eb4504d2bf45e08b314570ed688576391b00dcc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:49:25 +0000 Subject: [PATCH 09/21] Expand healthcheck module docstring scope Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/ca4099e0-8c02-470a-a2fa-c94c5d819746 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/healthcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/healthcheck.py b/src/healthcheck.py index 801b3c4..1003cbb 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -1,4 +1,4 @@ -"""Minimal HTTP healthcheck server for deployment probes.""" +"""HTTP healthcheck server for DB and Discord readiness, rate-limit, and heartbeat probes.""" import asyncio import json From 863b0887ebb3c687fb0ea41fab568c0173e897a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:51:01 +0000 Subject: [PATCH 10/21] Align module docstring ratelimit terminology Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/ca4099e0-8c02-470a-a2fa-c94c5d819746 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/healthcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/healthcheck.py b/src/healthcheck.py index 1003cbb..bb3ccec 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -1,4 +1,4 @@ -"""HTTP healthcheck server for DB and Discord readiness, rate-limit, and heartbeat probes.""" +"""HTTP healthcheck server for DB and Discord readiness, ratelimit, and heartbeat probes.""" import asyncio import json From 0324a43581911de59fbd71e11c595046f034ee1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:23:46 +0000 Subject: [PATCH 11/21] Refactor healthcheck startup into cog and add Docker HEALTHCHECK Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/d12d56d3-da0d-4c5d-b45a-8580f7b3f0ca Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- Dockerfile | 2 ++ src/__main__.py | 15 +++++---------- src/cogs/healthcheck.py | 31 +++++++++++++++++++++++++++++++ src/healthcheck.py | 8 ++++---- 4 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 src/cogs/healthcheck.py diff --git a/Dockerfile b/Dockerfile index 48e520c..141b567 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,4 +30,6 @@ USER appuser EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["python", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/', timeout=3).status == 200 else 1)"] + CMD ["python", "-m", "src"] diff --git a/src/__main__.py b/src/__main__.py index 69c0bf1..89b93b3 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -4,9 +4,9 @@ import discord from src import log_setup -from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT, TOKEN +from src.cogs.healthcheck import HealthcheckCog +from src.config import TOKEN from src.database import init_db, shutdown_db -from src.healthcheck import HealthcheckServer log_setup.setup_logging(logging.INFO) logger = logging.getLogger(__name__) @@ -33,23 +33,18 @@ async def on_ready() -> None: async def start() -> None: original_exc = None - healthcheck_server = HealthcheckServer( - bot, - host=HEALTHCHECK_HOST, - port=HEALTHCHECK_PORT, - path=HEALTHCHECK_PATH, - ) try: await init_db() - await healthcheck_server.start() async with bot: await bot.start(TOKEN) except Exception as e: # noqa: BLE001 original_exc = e finally: healthcheck_stop_exc = None + healthcheck_cog = bot.get_cog("healthcheck") try: - await healthcheck_server.stop() + if isinstance(healthcheck_cog, HealthcheckCog): + await healthcheck_cog.stop_server() except Exception as healthcheck_exc: # noqa: BLE001 healthcheck_stop_exc = healthcheck_exc diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py new file mode 100644 index 0000000..43f3c77 --- /dev/null +++ b/src/cogs/healthcheck.py @@ -0,0 +1,31 @@ +import logging + +import discord + +from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT +from src.healthcheck import HealthcheckServer + +logger = logging.getLogger(__name__) + + +class HealthcheckCog(discord.Cog, name="healthcheck"): + def __init__(self, bot: discord.Bot) -> None: + self.bot: discord.Bot = bot + self.healthcheck_server: HealthcheckServer = HealthcheckServer( + bot, + host=HEALTHCHECK_HOST, + port=HEALTHCHECK_PORT, + path=HEALTHCHECK_PATH, + ) + + @discord.Cog.listener(once=True) + async def on_connect(self) -> None: + await self.healthcheck_server.start() + logger.info("Healthcheck server started from healthcheck cog") + + async def stop_server(self) -> None: + await self.healthcheck_server.stop() + + +def setup(bot: discord.Bot) -> None: + bot.add_cog(HealthcheckCog(bot)) diff --git a/src/healthcheck.py b/src/healthcheck.py index bb3ccec..ff4f940 100644 --- a/src/healthcheck.py +++ b/src/healthcheck.py @@ -36,10 +36,10 @@ def __init__( :param port: TCP port for the healthcheck listener. :param path: HTTP path that serves health responses. """ - self.bot = bot - self.host = host - self.port = port - self.path = path + self.bot: discord.Bot = bot + self.host: str = host + self.port: int = port + self.path: str = path self._server: asyncio.AbstractServer | None = None async def start(self) -> None: From 9943694bb8e042b9b23790cb8a3cac428622db6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:24:56 +0000 Subject: [PATCH 12/21] Parameterize Docker healthcheck and cog name constant Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/d12d56d3-da0d-4c5d-b45a-8580f7b3f0ca Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- Dockerfile | 2 +- src/__main__.py | 4 ++-- src/cogs/healthcheck.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 141b567..2a97832 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,6 @@ USER appuser EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["python", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/', timeout=3).status == 200 else 1)"] +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["python", "-c", "import os, sys, urllib.request; host = os.getenv('HEALTHCHECK_HOST', '127.0.0.1'); port = os.getenv('HEALTHCHECK_PORT', '8080'); path = os.getenv('HEALTHCHECK_PATH', '/'); path = path if path.startswith('/') else '/' + path; url = f'http://{host}:{port}{path}'; sys.exit(0 if urllib.request.urlopen(url, timeout=3).status == 200 else 1)"] CMD ["python", "-m", "src"] diff --git a/src/__main__.py b/src/__main__.py index 89b93b3..c0dea3b 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -4,7 +4,7 @@ import discord from src import log_setup -from src.cogs.healthcheck import HealthcheckCog +from src.cogs.healthcheck import HEALTHCHECK_COG_NAME, HealthcheckCog from src.config import TOKEN from src.database import init_db, shutdown_db @@ -41,7 +41,7 @@ async def start() -> None: original_exc = e finally: healthcheck_stop_exc = None - healthcheck_cog = bot.get_cog("healthcheck") + healthcheck_cog = bot.get_cog(HEALTHCHECK_COG_NAME) try: if isinstance(healthcheck_cog, HealthcheckCog): await healthcheck_cog.stop_server() diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 43f3c77..1e362cb 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -6,9 +6,10 @@ from src.healthcheck import HealthcheckServer logger = logging.getLogger(__name__) +HEALTHCHECK_COG_NAME = "healthcheck" -class HealthcheckCog(discord.Cog, name="healthcheck"): +class HealthcheckCog(discord.Cog, name=HEALTHCHECK_COG_NAME): def __init__(self, bot: discord.Bot) -> None: self.bot: discord.Bot = bot self.healthcheck_server: HealthcheckServer = HealthcheckServer( From 671bea803b220d5b03a088ec27c418626fd02e30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:39:46 +0000 Subject: [PATCH 13/21] Switch Docker HEALTHCHECK to wget probe Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/6aaeb26f-7b63-47ec-b505-7c4b43280c81 Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2a97832..4e9efcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,8 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/* + RUN adduser -u 8192 --disabled-password --gecos "" appuser && chown -R appuser /app COPY --from=python-base --chown=appuser /app/requirements.txt ./ @@ -30,6 +32,6 @@ USER appuser EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["python", "-c", "import os, sys, urllib.request; host = os.getenv('HEALTHCHECK_HOST', '127.0.0.1'); port = os.getenv('HEALTHCHECK_PORT', '8080'); path = os.getenv('HEALTHCHECK_PATH', '/'); path = path if path.startswith('/') else '/' + path; url = f'http://{host}:{port}{path}'; sys.exit(0 if urllib.request.urlopen(url, timeout=3).status == 200 else 1)"] +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["sh", "-c", "path=\"${HEALTHCHECK_PATH:-/}\"; case \"$path\" in /*) ;; *) path=\"/$path\" ;; esac; wget -q -T 3 -O /dev/null \"http://${HEALTHCHECK_HOST:-127.0.0.1}:${HEALTHCHECK_PORT:-8080}${path}\""] CMD ["python", "-m", "src"] From 372b9e5d27255a79b0d464a84ef893aed089a7a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:00:43 +0000 Subject: [PATCH 14/21] Apply healthcheck review changes for Docker defaults and endpoint path Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/a3887bc1-9eae-4159-b51b-265684de04ab Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- .env.example | 1 - Dockerfile | 4 +++- README.md | 3 +-- src/cogs/healthcheck.py | 5 ++--- src/config.py | 4 ---- src/{healthcheck.py => runtime_healthcheck.py} | 4 ++-- 6 files changed, 8 insertions(+), 13 deletions(-) rename src/{healthcheck.py => runtime_healthcheck.py} (98%) diff --git a/.env.example b/.env.example index 1e7441a..4ce48a7 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ TOKEN="YOUR BOT TOKEN HERE" HEALTHCHECK_HOST="127.0.0.1" HEALTHCHECK_PORT="8080" -HEALTHCHECK_PATH="/" diff --git a/Dockerfile b/Dockerfile index 4e9efcc..56b2614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ FROM python:${PYTHON_VERSION}-slim-bookworm AS python-base ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 +ENV HEALTHCHECK_HOST=127.0.0.1 +ENV HEALTHCHECK_PORT=8080 RUN pip install uv @@ -32,6 +34,6 @@ USER appuser EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["sh", "-c", "path=\"${HEALTHCHECK_PATH:-/}\"; case \"$path\" in /*) ;; *) path=\"/$path\" ;; esac; wget -q -T 3 -O /dev/null \"http://${HEALTHCHECK_HOST:-127.0.0.1}:${HEALTHCHECK_PORT:-8080}${path}\""] +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["sh", "-c", "wget -q -T 3 -O /dev/null \"http://${HEALTHCHECK_HOST}:${HEALTHCHECK_PORT}/health\""] CMD ["python", "-m", "src"] diff --git a/README.md b/README.md index 7db7ca1..fc5a1f8 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,11 @@ Versa is a simple utility Discord bot, with the main goal of being open source a The bot exposes an HTTP healthcheck endpoint for deployment platforms (such as Coolify). -- Method/path: `GET /` (configurable via `HEALTHCHECK_PATH`) +- Method/path: `GET /health` - Default bind: `127.0.0.1:8080` - Config via env vars: - `HEALTHCHECK_HOST` - `HEALTHCHECK_PORT` - - `HEALTHCHECK_PATH` The endpoint returns: - `200` when DB is responsive, Discord is connected, Discord shard heartbeat latency is healthy, and no global Discord rate-limit is active diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 1e362cb..8a01fd6 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -2,8 +2,8 @@ import discord -from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PATH, HEALTHCHECK_PORT -from src.healthcheck import HealthcheckServer +from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PORT +from src.runtime_healthcheck import HealthcheckServer logger = logging.getLogger(__name__) HEALTHCHECK_COG_NAME = "healthcheck" @@ -16,7 +16,6 @@ def __init__(self, bot: discord.Bot) -> None: bot, host=HEALTHCHECK_HOST, port=HEALTHCHECK_PORT, - path=HEALTHCHECK_PATH, ) @discord.Cog.listener(once=True) diff --git a/src/config.py b/src/config.py index 0be289d..909dfdf 100644 --- a/src/config.py +++ b/src/config.py @@ -12,7 +12,3 @@ HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST", "127.0.0.1") HEALTHCHECK_PORT = int(os.getenv("HEALTHCHECK_PORT", "8080")) -HEALTHCHECK_PATH = os.getenv("HEALTHCHECK_PATH", "/") - -if not HEALTHCHECK_PATH.startswith("/"): - HEALTHCHECK_PATH = f"/{HEALTHCHECK_PATH}" diff --git a/src/healthcheck.py b/src/runtime_healthcheck.py similarity index 98% rename from src/healthcheck.py rename to src/runtime_healthcheck.py index ff4f940..160a0b6 100644 --- a/src/healthcheck.py +++ b/src/runtime_healthcheck.py @@ -1,4 +1,4 @@ -"""HTTP healthcheck server for DB and Discord readiness, ratelimit, and heartbeat probes.""" +"""Runtime HTTP healthcheck server for DB and Discord readiness probes.""" import asyncio import json @@ -26,7 +26,7 @@ def __init__( *, host: str, port: int, - path: str = "/", + path: str = "/health", ) -> None: """ Initialize the healthcheck server. From e9781b57b48055fb2cac442274ae6bb2e3416011 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:41:31 +0000 Subject: [PATCH 15/21] Allow disabling healthcheck via empty host and remove app-side defaults Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- README.md | 3 ++- src/cogs/healthcheck.py | 22 +++++++++++++++++----- src/config.py | 4 ++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fc5a1f8..412b9cb 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ Versa is a simple utility Discord bot, with the main goal of being open source a The bot exposes an HTTP healthcheck endpoint for deployment platforms (such as Coolify). - Method/path: `GET /health` -- Default bind: `127.0.0.1:8080` +- Docker default bind: `127.0.0.1:8080` (set via `ENV` in `Dockerfile`) - Config via env vars: - `HEALTHCHECK_HOST` - `HEALTHCHECK_PORT` + - Leave `HEALTHCHECK_HOST` unset/empty to disable the healthcheck server The endpoint returns: - `200` when DB is responsive, Discord is connected, Discord shard heartbeat latency is healthy, and no global Discord rate-limit is active diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 8a01fd6..736064f 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -12,18 +12,30 @@ class HealthcheckCog(discord.Cog, name=HEALTHCHECK_COG_NAME): def __init__(self, bot: discord.Bot) -> None: self.bot: discord.Bot = bot - self.healthcheck_server: HealthcheckServer = HealthcheckServer( - bot, - host=HEALTHCHECK_HOST, - port=HEALTHCHECK_PORT, - ) + self.healthcheck_server: HealthcheckServer | None = None + if HEALTHCHECK_HOST: + if HEALTHCHECK_PORT is None: + msg = "HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured" + raise RuntimeError(msg) + + self.healthcheck_server = HealthcheckServer( + bot, + host=HEALTHCHECK_HOST, + port=int(HEALTHCHECK_PORT), + ) @discord.Cog.listener(once=True) async def on_connect(self) -> None: + if self.healthcheck_server is None: + logger.info("Healthcheck server disabled because HEALTHCHECK_HOST is unset/empty") + return + await self.healthcheck_server.start() logger.info("Healthcheck server started from healthcheck cog") async def stop_server(self) -> None: + if self.healthcheck_server is None: + return await self.healthcheck_server.stop() diff --git a/src/config.py b/src/config.py index 909dfdf..38a3f09 100644 --- a/src/config.py +++ b/src/config.py @@ -10,5 +10,5 @@ DB_PATH = Path(os.getenv("DB_PATH") or Path("data/database.db")).absolute() DB_PATH.parent.mkdir(parents=True, exist_ok=True) -HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST", "127.0.0.1") -HEALTHCHECK_PORT = int(os.getenv("HEALTHCHECK_PORT", "8080")) +HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST") +HEALTHCHECK_PORT = os.getenv("HEALTHCHECK_PORT") From 1f61ca715c7f5888986a91be396ec58a9b2775ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:42:37 +0000 Subject: [PATCH 16/21] Refine healthcheck env typing and clarify disable docs Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- README.md | 2 +- src/cogs/healthcheck.py | 2 +- src/config.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 412b9cb..b1e7150 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The bot exposes an HTTP healthcheck endpoint for deployment platforms (such as C - Config via env vars: - `HEALTHCHECK_HOST` - `HEALTHCHECK_PORT` - - Leave `HEALTHCHECK_HOST` unset/empty to disable the healthcheck server + - Leave `HEALTHCHECK_HOST` unset or set to an empty string to disable the healthcheck server The endpoint returns: - `200` when DB is responsive, Discord is connected, Discord shard heartbeat latency is healthy, and no global Discord rate-limit is active diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 736064f..8cfe78f 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -21,7 +21,7 @@ def __init__(self, bot: discord.Bot) -> None: self.healthcheck_server = HealthcheckServer( bot, host=HEALTHCHECK_HOST, - port=int(HEALTHCHECK_PORT), + port=HEALTHCHECK_PORT, ) @discord.Cog.listener(once=True) diff --git a/src/config.py b/src/config.py index 38a3f09..be52268 100644 --- a/src/config.py +++ b/src/config.py @@ -11,4 +11,5 @@ DB_PATH.parent.mkdir(parents=True, exist_ok=True) HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST") -HEALTHCHECK_PORT = os.getenv("HEALTHCHECK_PORT") +HEALTHCHECK_PORT_RAW = os.getenv("HEALTHCHECK_PORT") +HEALTHCHECK_PORT = int(HEALTHCHECK_PORT_RAW) if HEALTHCHECK_PORT_RAW else None From 85443ee6d914dbfe80004c8f60064e14ac4a72e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:43:23 +0000 Subject: [PATCH 17/21] Clarify env variable name in healthcheck port error Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/cogs/healthcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 8cfe78f..419418c 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -15,7 +15,7 @@ def __init__(self, bot: discord.Bot) -> None: self.healthcheck_server: HealthcheckServer | None = None if HEALTHCHECK_HOST: if HEALTHCHECK_PORT is None: - msg = "HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured" + msg = "Environment variable HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured" raise RuntimeError(msg) self.healthcheck_server = HealthcheckServer( From e7d5a497badb599b87ae477b1dc91db307d8462a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:44:07 +0000 Subject: [PATCH 18/21] Include configured host value in healthcheck port error Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/cogs/healthcheck.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 419418c..bdbd41b 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -15,7 +15,10 @@ def __init__(self, bot: discord.Bot) -> None: self.healthcheck_server: HealthcheckServer | None = None if HEALTHCHECK_HOST: if HEALTHCHECK_PORT is None: - msg = "Environment variable HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured" + msg = ( + "Environment variable HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured " + f"(current value: {HEALTHCHECK_HOST})" + ) raise RuntimeError(msg) self.healthcheck_server = HealthcheckServer( From 96b148c2eeb3c02fa041a1f5e0425f3bef16f83d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:44:58 +0000 Subject: [PATCH 19/21] Validate HEALTHCHECK_PORT in cog with explicit runtime errors Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/cogs/healthcheck.py | 21 ++++++++++++++++----- src/config.py | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index bdbd41b..a27fc80 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -2,7 +2,7 @@ import discord -from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PORT +from src.config import HEALTHCHECK_HOST, HEALTHCHECK_PORT_RAW from src.runtime_healthcheck import HealthcheckServer logger = logging.getLogger(__name__) @@ -14,17 +14,28 @@ def __init__(self, bot: discord.Bot) -> None: self.bot: discord.Bot = bot self.healthcheck_server: HealthcheckServer | None = None if HEALTHCHECK_HOST: - if HEALTHCHECK_PORT is None: + if HEALTHCHECK_PORT_RAW is None: msg = ( - "Environment variable HEALTHCHECK_PORT must be set when HEALTHCHECK_HOST is configured " - f"(current value: {HEALTHCHECK_HOST})" + "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " + "HEALTHCHECK_HOST is configured " + f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" ) raise RuntimeError(msg) + try: + healthcheck_port = int(HEALTHCHECK_PORT_RAW) + except ValueError as e: + msg = ( + "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " + "HEALTHCHECK_HOST is configured " + f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" + ) + raise RuntimeError(msg) from e + self.healthcheck_server = HealthcheckServer( bot, host=HEALTHCHECK_HOST, - port=HEALTHCHECK_PORT, + port=healthcheck_port, ) @discord.Cog.listener(once=True) diff --git a/src/config.py b/src/config.py index be52268..500eff2 100644 --- a/src/config.py +++ b/src/config.py @@ -12,4 +12,3 @@ HEALTHCHECK_HOST = os.getenv("HEALTHCHECK_HOST") HEALTHCHECK_PORT_RAW = os.getenv("HEALTHCHECK_PORT") -HEALTHCHECK_PORT = int(HEALTHCHECK_PORT_RAW) if HEALTHCHECK_PORT_RAW else None From 0d8ec97b8b7fd4c6b3694b625dd0bbfb07a54245 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:45:42 +0000 Subject: [PATCH 20/21] Deduplicate healthcheck port validation error message Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/cogs/healthcheck.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index a27fc80..214aea6 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -14,23 +14,18 @@ def __init__(self, bot: discord.Bot) -> None: self.bot: discord.Bot = bot self.healthcheck_server: HealthcheckServer | None = None if HEALTHCHECK_HOST: + port_error_msg = ( + "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " + "HEALTHCHECK_HOST is configured " + f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" + ) if HEALTHCHECK_PORT_RAW is None: - msg = ( - "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " - "HEALTHCHECK_HOST is configured " - f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" - ) - raise RuntimeError(msg) + raise RuntimeError(port_error_msg) try: healthcheck_port = int(HEALTHCHECK_PORT_RAW) except ValueError as e: - msg = ( - "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " - "HEALTHCHECK_HOST is configured " - f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" - ) - raise RuntimeError(msg) from e + raise RuntimeError(port_error_msg) from e self.healthcheck_server = HealthcheckServer( bot, From e2e2cfb325339cbce7797b81ef635a694f954a3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:46:25 +0000 Subject: [PATCH 21/21] Differentiate missing vs invalid HEALTHCHECK_PORT errors Agent-Logs-Url: https://github.com/Versa-Bots/versa/sessions/56e0aa53-8e42-43cb-80a0-de126250306f Co-authored-by: ToothyDev <55001472+ToothyDev@users.noreply.github.com> --- src/cogs/healthcheck.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cogs/healthcheck.py b/src/cogs/healthcheck.py index 214aea6..8c599d3 100644 --- a/src/cogs/healthcheck.py +++ b/src/cogs/healthcheck.py @@ -14,18 +14,22 @@ def __init__(self, bot: discord.Bot) -> None: self.bot: discord.Bot = bot self.healthcheck_server: HealthcheckServer | None = None if HEALTHCHECK_HOST: - port_error_msg = ( - "Environment variable HEALTHCHECK_PORT must be set to a valid integer when " - "HEALTHCHECK_HOST is configured " - f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" - ) if HEALTHCHECK_PORT_RAW is None: - raise RuntimeError(port_error_msg) + msg = ( + "Environment variable HEALTHCHECK_PORT must be set when " + f"HEALTHCHECK_HOST is configured (HEALTHCHECK_HOST={HEALTHCHECK_HOST})" + ) + raise RuntimeError(msg) try: healthcheck_port = int(HEALTHCHECK_PORT_RAW) except ValueError as e: - raise RuntimeError(port_error_msg) from e + msg = ( + "Environment variable HEALTHCHECK_PORT must be a valid integer when " + "HEALTHCHECK_HOST is configured " + f"(HEALTHCHECK_HOST={HEALTHCHECK_HOST}, HEALTHCHECK_PORT={HEALTHCHECK_PORT_RAW})" + ) + raise RuntimeError(msg) from e self.healthcheck_server = HealthcheckServer( bot,