From 0038a1889c6c0b480aea9c8dc5668ec68e18df0a Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Mon, 11 Aug 2025 01:32:16 +0200 Subject: [PATCH] 0.3.9.5.a3 Transitioned from RSS-based commit message fetching to API-based fetching for commit messages --- bot.py | 2 +- modules/common/boot_notice.py | 150 ++++++++++++++++------------------ 2 files changed, 73 insertions(+), 79 deletions(-) diff --git a/bot.py b/bot.py index 05ebde8..a96f3bd 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) -VERSION = "0.3.9.5.a2" +VERSION = "0.3.9.5.a3" # ---------- Env loading ---------- diff --git a/modules/common/boot_notice.py b/modules/common/boot_notice.py index a98a3ac..aad0260 100644 --- a/modules/common/boot_notice.py +++ b/modules/common/boot_notice.py @@ -4,7 +4,7 @@ import re import base64 import json import time -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from urllib.parse import urlparse import discord @@ -38,23 +38,20 @@ def _split_subject_body(full_message: str) -> tuple[str | None, str | None]: def _cmp_versions(a: str | None, b: str | None) -> int: """ - Compare your version style: 1.2.3.4.a2 (last segment alnum optional). - Returns: -1 if ab. - If either is None, treat as equal (0) to avoid false rollback/upgrade. + Compare versions like 1.2.3.4.a2 (last segment may be alnum). + Returns -1 if ab. + If either is None, treat as equal to avoid false positives. """ if not a or not b: return 0 pa = a.split('.') pb = b.split('.') - # pad to 5 parts while len(pa) < 5: pa.append('0') while len(pb) < 5: pb.append('0') def part_key(x: str): - # numeric if digits; else (numeric_prefix, alpha_suffix) if x.isdigit(): return (int(x), '', 1) - # split alnum: digits prefix (if any) + rest m = re.match(r'(\d+)(.*)', x) if m: return (int(m.group(1)), m.group(2), 2) @@ -65,7 +62,7 @@ def _cmp_versions(a: str | None, b: str | None) -> int: if ka[0] != kb[0]: return 1 if ka[0] > kb[0] else -1 if ka[2] != kb[2]: - return 1 if ka[2] < kb[2] else -1 # prefer pure numeric (1) > num+alpha (2) > alpha (3) + return 1 if ka[2] < kb[2] else -1 if ka[1] != kb[1]: return 1 if ka[1] > kb[1] else -1 return 0 @@ -106,48 +103,51 @@ async def _gitea_get_json(url: str, token: str | None, user: str | None, timeout if resp.status != 200: text = await resp.text() raise RuntimeError(f"Gitea GET {url} -> {resp.status}: {text[:200]}") + # Gitea returns either dict or list depending on endpoint + ctype = resp.headers.get("Content-Type", "") + if "application/json" not in ctype: + text = await resp.text() + raise RuntimeError(f"Gitea GET {url} non-JSON: {ctype} {text[:200]}") return await resp.json() async def _fetch_latest_commit(api_base: str, owner: str, repo: str, branch: str, token: str | None, user: str | None) -> tuple[str | None, str | None, str | None]: """ - Returns (sha, subject, body) for the latest commit on branch. + Returns (sha, subject, body) for the latest commit on branch using the + list-commits endpoint which includes the full commit message: + /api/v1/repos/{owner}/{repo}/commits?sha={branch}&limit=1&stat=false&verification=false&files=false """ - # Fast path: get branch -> commit sha - branch_url = f"{api_base}/repos/{owner}/{repo}/branches/{branch}" - try: - bjson = await _gitea_get_json(branch_url, token, user) - sha = bjson.get('commit', {}).get('id') or bjson.get('commit', {}).get('sha') - if not sha: - raise RuntimeError("No commit sha on branch") - except Exception as e: - # Fallback: list commits - commits_url = f"{api_base}/repos/{owner}/{repo}/commits/{branch}" - try: - cjson = await _gitea_get_json(commits_url, token, user) - if isinstance(cjson, list) and cjson: - sha = cjson[0].get('sha') or cjson[0].get('id') - else: - raise RuntimeError("Empty commits list") - except Exception as e2: - raise RuntimeError(f"Failed to get latest commit: {e} / {e2}") - - # Now fetch full commit message - # Try git/commits first - for endpoint in (f"{api_base}/repos/{owner}/{repo}/git/commits/{sha}", - f"{api_base}/repos/{owner}/{repo}/commits/{sha}"): - try: - data = await _gitea_get_json(endpoint, token, user) - msg = None - if isinstance(data, dict): - msg = data.get('message') - if not msg: - msg = data.get('commit', {}).get('message') - subject, body = _split_subject_body(msg or "") - return sha, (subject or ""), (body or "") - except Exception: - continue - raise RuntimeError("Unable to fetch commit details") + url = ( + f"{api_base}/repos/{owner}/{repo}/commits" + f"?sha={branch}&limit=1&stat=false&verification=false&files=false" + ) + data = await _gitea_get_json(url, token, user) + if not isinstance(data, list) or not data: + raise RuntimeError("Commits list empty or invalid.") + latest = data[0] + sha = latest.get("sha") or latest.get("id") + message = "" + commit_obj = latest.get("commit") or {} + if isinstance(commit_obj, dict): + message = commit_obj.get("message") or "" + if not message: + # Extremely unlikely on this endpoint, but try secondary fetch + if sha: + for endpoint in ( + f"{api_base}/repos/{owner}/{repo}/git/commits/{sha}", + f"{api_base}/repos/{owner}/{repo}/commits/{sha}", + ): + try: + det = await _gitea_get_json(endpoint, token, user) + if isinstance(det, dict): + m = det.get("message") or det.get("commit", {}).get("message") + if m: + message = str(m) + break + except Exception: + continue + subject, body = _split_subject_body(message) + return sha, (subject or ""), (body or "") # ---------------- Boot reason inference ---------------- @@ -178,17 +178,12 @@ def _format_status_line(kind: str, old_ver: str | None, new_ver: str | None) -> async def post_boot_notice(bot): """ Always post a boot status to the modlog channel. - Logic: - - Wait until bot is ready (guilds/channels cached). - - Resolve repo from cfg(repo_url/repo_branch); attempt to fetch latest commit (sha, subject, body). - - Compare to stored boot_state (last_sha/last_version/last_boot_ts): - * sha/version advanced -> Updated - * sha same and near scheduled time -> Scheduled restart - * sha same and not near schedule -> Manual restart - * version decreased -> Rollback (ping guild owner) - - Post status line. - - Post commit message (bold version + md body). - - Persist new boot_state. + - Waits for ready. + - Infers reason (updated/scheduled/manual/rollback). + - Fetches latest commit (full message) via list-commits endpoint. + - Posts status + commit message (Version bold + md details). + - Pings guild owner only on rollback. + - Persists last sha/version in data_manager['boot_state']. """ try: await bot.wait_until_ready() @@ -203,7 +198,6 @@ async def post_boot_notice(bot): ch = bot.get_channel(modlog_channel_id) if not ch: - # fallback: search across guilds for g in bot.guilds: ch = g.get_channel(modlog_channel_id) if ch: @@ -216,17 +210,19 @@ async def post_boot_notice(bot): r = cfg(bot) repo_url = r.get('repo_url', '') branch = r.get('repo_branch', 'main') - api_base = owner = repo = None - - if repo_url: - api_base, owner, repo = _parse_repo_url(repo_url) - - token = os.getenv("SHAI_GITEA_TOKEN", "").strip() or None - user = os.getenv("SHAI_GITEA_USER", "").strip() or None check_time_utc = r.get('check_time_utc', '') # e.g., "03:00" now_utc = datetime.now(timezone.utc) - # State store + api_base = owner = repo = None + if repo_url: + api_base, owner, repo = _parse_repo_url(repo_url) + else: + print("[boot_notice] repo_url missing; commit lookup skipped.") + + token = os.getenv("SHAI_GITEA_TOKEN", "").strip() or None + user = os.getenv("SHAI_GITEA_USER", "").strip() or None + + # State dm = getattr(bot, "data_manager", None) if not dm: print("[boot_notice] data_manager missing on bot; cannot persist state.") @@ -243,8 +239,9 @@ async def post_boot_notice(bot): sha, subject, body = await _fetch_latest_commit(api_base, owner, repo, branch, token, user) except Exception as e: print(f"[boot_notice] fetch latest commit failed: {e}") + else: + print("[boot_notice] repo parsing failed; commit lookup skipped.") - # Derive current version (from subject) curr_ver = _extract_version(subject) if subject else None # Decide reason @@ -258,10 +255,9 @@ async def post_boot_notice(bot): elif cmpv > 0: reason = "rollback" mention_owner = True - else: # same version + else: reason = "scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual" else: - # Fall back to sha compare if versions missing if prev_sha and sha and prev_sha != sha: reason = "updated" else: @@ -270,8 +266,12 @@ async def post_boot_notice(bot): # Post status line status_line = _format_status_line(reason, prev_ver, curr_ver) try: - # ping owner only on rollback - allowed = discord.AllowedMentions(everyone=False, users=True if mention_owner else False, roles=False, replied_user=False) + allowed = discord.AllowedMentions( + everyone=False, + users=True if mention_owner else False, + roles=False, + replied_user=False + ) if mention_owner and ch.guild and ch.guild.owner_id: status_line = f"{status_line}\n<@{ch.guild.owner_id}>" await ch.send(status_line, allowed_mentions=allowed) @@ -279,21 +279,16 @@ async def post_boot_notice(bot): print(f"[boot_notice] failed to send status line: {e}") return - # Post commit message (if we have it) - # Format: **Version**\n + # Post commit message (Version + md details) try: title = curr_ver or (subject or "Latest commit") if title or body: - # Always post a commit message on start; it’s the core “what’s running now” - if body: - commit_msg = f"**{title}**\n{body}" - else: - commit_msg = f"**{title}**" + commit_msg = f"**{title}**\n{body}" if body else f"**{title}**" await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none()) except Exception as e: print(f"[boot_notice] failed to send commit message: {e}") - # Persist new state + # Persist state try: new_state = { 'last_sha': sha, @@ -301,7 +296,6 @@ async def post_boot_notice(bot): 'last_subject': subject, 'last_boot_ts': time.time(), } - # keep boot_state as list to preserve history dm.add('boot_state', new_state) except Exception as e: print(f"[boot_notice] failed to persist boot_state: {e}")