0.3.9.5.a3
Transitioned from RSS-based commit message fetching to API-based fetching for commit messages
This commit is contained in:
parent
2a898802b6
commit
0038a1889c
2
bot.py
2
bot.py
@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
|
|||||||
|
|
||||||
# Version consists of:
|
# Version consists of:
|
||||||
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update)
|
# 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 ----------
|
# ---------- Env loading ----------
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import re
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import discord
|
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:
|
def _cmp_versions(a: str | None, b: str | None) -> int:
|
||||||
"""
|
"""
|
||||||
Compare your version style: 1.2.3.4.a2 (last segment alnum optional).
|
Compare versions like 1.2.3.4.a2 (last segment may be alnum).
|
||||||
Returns: -1 if a<b, 0 if equal/unknown, +1 if a>b.
|
Returns -1 if a<b, 0 if equal/unknown, +1 if a>b.
|
||||||
If either is None, treat as equal (0) to avoid false rollback/upgrade.
|
If either is None, treat as equal to avoid false positives.
|
||||||
"""
|
"""
|
||||||
if not a or not b:
|
if not a or not b:
|
||||||
return 0
|
return 0
|
||||||
pa = a.split('.')
|
pa = a.split('.')
|
||||||
pb = b.split('.')
|
pb = b.split('.')
|
||||||
# pad to 5 parts
|
|
||||||
while len(pa) < 5: pa.append('0')
|
while len(pa) < 5: pa.append('0')
|
||||||
while len(pb) < 5: pb.append('0')
|
while len(pb) < 5: pb.append('0')
|
||||||
|
|
||||||
def part_key(x: str):
|
def part_key(x: str):
|
||||||
# numeric if digits; else (numeric_prefix, alpha_suffix)
|
|
||||||
if x.isdigit():
|
if x.isdigit():
|
||||||
return (int(x), '', 1)
|
return (int(x), '', 1)
|
||||||
# split alnum: digits prefix (if any) + rest
|
|
||||||
m = re.match(r'(\d+)(.*)', x)
|
m = re.match(r'(\d+)(.*)', x)
|
||||||
if m:
|
if m:
|
||||||
return (int(m.group(1)), m.group(2), 2)
|
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]:
|
if ka[0] != kb[0]:
|
||||||
return 1 if ka[0] > kb[0] else -1
|
return 1 if ka[0] > kb[0] else -1
|
||||||
if ka[2] != kb[2]:
|
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]:
|
if ka[1] != kb[1]:
|
||||||
return 1 if ka[1] > kb[1] else -1
|
return 1 if ka[1] > kb[1] else -1
|
||||||
return 0
|
return 0
|
||||||
@ -106,48 +103,51 @@ async def _gitea_get_json(url: str, token: str | None, user: str | None, timeout
|
|||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
raise RuntimeError(f"Gitea GET {url} -> {resp.status}: {text[:200]}")
|
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()
|
return await resp.json()
|
||||||
|
|
||||||
async def _fetch_latest_commit(api_base: str, owner: str, repo: str, branch: str,
|
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]:
|
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
|
url = (
|
||||||
branch_url = f"{api_base}/repos/{owner}/{repo}/branches/{branch}"
|
f"{api_base}/repos/{owner}/{repo}/commits"
|
||||||
try:
|
f"?sha={branch}&limit=1&stat=false&verification=false&files=false"
|
||||||
bjson = await _gitea_get_json(branch_url, token, user)
|
)
|
||||||
sha = bjson.get('commit', {}).get('id') or bjson.get('commit', {}).get('sha')
|
data = await _gitea_get_json(url, token, user)
|
||||||
if not sha:
|
if not isinstance(data, list) or not data:
|
||||||
raise RuntimeError("No commit sha on branch")
|
raise RuntimeError("Commits list empty or invalid.")
|
||||||
except Exception as e:
|
latest = data[0]
|
||||||
# Fallback: list commits
|
sha = latest.get("sha") or latest.get("id")
|
||||||
commits_url = f"{api_base}/repos/{owner}/{repo}/commits/{branch}"
|
message = ""
|
||||||
try:
|
commit_obj = latest.get("commit") or {}
|
||||||
cjson = await _gitea_get_json(commits_url, token, user)
|
if isinstance(commit_obj, dict):
|
||||||
if isinstance(cjson, list) and cjson:
|
message = commit_obj.get("message") or ""
|
||||||
sha = cjson[0].get('sha') or cjson[0].get('id')
|
if not message:
|
||||||
else:
|
# Extremely unlikely on this endpoint, but try secondary fetch
|
||||||
raise RuntimeError("Empty commits list")
|
if sha:
|
||||||
except Exception as e2:
|
for endpoint in (
|
||||||
raise RuntimeError(f"Failed to get latest commit: {e} / {e2}")
|
f"{api_base}/repos/{owner}/{repo}/git/commits/{sha}",
|
||||||
|
f"{api_base}/repos/{owner}/{repo}/commits/{sha}",
|
||||||
# Now fetch full commit message
|
):
|
||||||
# Try git/commits first
|
try:
|
||||||
for endpoint in (f"{api_base}/repos/{owner}/{repo}/git/commits/{sha}",
|
det = await _gitea_get_json(endpoint, token, user)
|
||||||
f"{api_base}/repos/{owner}/{repo}/commits/{sha}"):
|
if isinstance(det, dict):
|
||||||
try:
|
m = det.get("message") or det.get("commit", {}).get("message")
|
||||||
data = await _gitea_get_json(endpoint, token, user)
|
if m:
|
||||||
msg = None
|
message = str(m)
|
||||||
if isinstance(data, dict):
|
break
|
||||||
msg = data.get('message')
|
except Exception:
|
||||||
if not msg:
|
continue
|
||||||
msg = data.get('commit', {}).get('message')
|
subject, body = _split_subject_body(message)
|
||||||
subject, body = _split_subject_body(msg or "")
|
return sha, (subject or ""), (body or "")
|
||||||
return sha, (subject or ""), (body or "")
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
raise RuntimeError("Unable to fetch commit details")
|
|
||||||
|
|
||||||
# ---------------- Boot reason inference ----------------
|
# ---------------- 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):
|
async def post_boot_notice(bot):
|
||||||
"""
|
"""
|
||||||
Always post a boot status to the modlog channel.
|
Always post a boot status to the modlog channel.
|
||||||
Logic:
|
- Waits for ready.
|
||||||
- Wait until bot is ready (guilds/channels cached).
|
- Infers reason (updated/scheduled/manual/rollback).
|
||||||
- Resolve repo from cfg(repo_url/repo_branch); attempt to fetch latest commit (sha, subject, body).
|
- Fetches latest commit (full message) via list-commits endpoint.
|
||||||
- Compare to stored boot_state (last_sha/last_version/last_boot_ts):
|
- Posts status + commit message (Version bold + md details).
|
||||||
* sha/version advanced -> Updated
|
- Pings guild owner only on rollback.
|
||||||
* sha same and near scheduled time -> Scheduled restart
|
- Persists last sha/version in data_manager['boot_state'].
|
||||||
* 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.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await bot.wait_until_ready()
|
await bot.wait_until_ready()
|
||||||
@ -203,7 +198,6 @@ async def post_boot_notice(bot):
|
|||||||
|
|
||||||
ch = bot.get_channel(modlog_channel_id)
|
ch = bot.get_channel(modlog_channel_id)
|
||||||
if not ch:
|
if not ch:
|
||||||
# fallback: search across guilds
|
|
||||||
for g in bot.guilds:
|
for g in bot.guilds:
|
||||||
ch = g.get_channel(modlog_channel_id)
|
ch = g.get_channel(modlog_channel_id)
|
||||||
if ch:
|
if ch:
|
||||||
@ -216,17 +210,19 @@ async def post_boot_notice(bot):
|
|||||||
r = cfg(bot)
|
r = cfg(bot)
|
||||||
repo_url = r.get('repo_url', '')
|
repo_url = r.get('repo_url', '')
|
||||||
branch = r.get('repo_branch', 'main')
|
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"
|
check_time_utc = r.get('check_time_utc', '') # e.g., "03:00"
|
||||||
now_utc = datetime.now(timezone.utc)
|
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)
|
dm = getattr(bot, "data_manager", None)
|
||||||
if not dm:
|
if not dm:
|
||||||
print("[boot_notice] data_manager missing on bot; cannot persist state.")
|
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)
|
sha, subject, body = await _fetch_latest_commit(api_base, owner, repo, branch, token, user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[boot_notice] fetch latest commit failed: {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
|
curr_ver = _extract_version(subject) if subject else None
|
||||||
|
|
||||||
# Decide reason
|
# Decide reason
|
||||||
@ -258,10 +255,9 @@ async def post_boot_notice(bot):
|
|||||||
elif cmpv > 0:
|
elif cmpv > 0:
|
||||||
reason = "rollback"
|
reason = "rollback"
|
||||||
mention_owner = True
|
mention_owner = True
|
||||||
else: # same version
|
else:
|
||||||
reason = "scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"
|
reason = "scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"
|
||||||
else:
|
else:
|
||||||
# Fall back to sha compare if versions missing
|
|
||||||
if prev_sha and sha and prev_sha != sha:
|
if prev_sha and sha and prev_sha != sha:
|
||||||
reason = "updated"
|
reason = "updated"
|
||||||
else:
|
else:
|
||||||
@ -270,8 +266,12 @@ async def post_boot_notice(bot):
|
|||||||
# Post status line
|
# Post status line
|
||||||
status_line = _format_status_line(reason, prev_ver, curr_ver)
|
status_line = _format_status_line(reason, prev_ver, curr_ver)
|
||||||
try:
|
try:
|
||||||
# ping owner only on rollback
|
allowed = discord.AllowedMentions(
|
||||||
allowed = discord.AllowedMentions(everyone=False, users=True if mention_owner else False, roles=False, replied_user=False)
|
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:
|
if mention_owner and ch.guild and ch.guild.owner_id:
|
||||||
status_line = f"{status_line}\n<@{ch.guild.owner_id}>"
|
status_line = f"{status_line}\n<@{ch.guild.owner_id}>"
|
||||||
await ch.send(status_line, allowed_mentions=allowed)
|
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}")
|
print(f"[boot_notice] failed to send status line: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Post commit message (if we have it)
|
# Post commit message (Version + md details)
|
||||||
# Format: **Version**\n<md body>
|
|
||||||
try:
|
try:
|
||||||
title = curr_ver or (subject or "Latest commit")
|
title = curr_ver or (subject or "Latest commit")
|
||||||
if title or body:
|
if title or body:
|
||||||
# Always post a commit message on start; it’s the core “what’s running now”
|
commit_msg = f"**{title}**\n{body}" if body else f"**{title}**"
|
||||||
if body:
|
|
||||||
commit_msg = f"**{title}**\n{body}"
|
|
||||||
else:
|
|
||||||
commit_msg = f"**{title}**"
|
|
||||||
await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none())
|
await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[boot_notice] failed to send commit message: {e}")
|
print(f"[boot_notice] failed to send commit message: {e}")
|
||||||
|
|
||||||
# Persist new state
|
# Persist state
|
||||||
try:
|
try:
|
||||||
new_state = {
|
new_state = {
|
||||||
'last_sha': sha,
|
'last_sha': sha,
|
||||||
@ -301,7 +296,6 @@ async def post_boot_notice(bot):
|
|||||||
'last_subject': subject,
|
'last_subject': subject,
|
||||||
'last_boot_ts': time.time(),
|
'last_boot_ts': time.time(),
|
||||||
}
|
}
|
||||||
# keep boot_state as list to preserve history
|
|
||||||
dm.add('boot_state', new_state)
|
dm.add('boot_state', new_state)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[boot_notice] failed to persist boot_state: {e}")
|
print(f"[boot_notice] failed to persist boot_state: {e}")
|
||||||
|
Loading…
Reference in New Issue
Block a user