Compare commits
1 Commits
fix/534
...
fix/662-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edca963e00 |
110
scripts/backlog_cleanup.py
Executable file
110
scripts/backlog_cleanup.py
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Backlog Cleanup — Bulk close issues whose PRs are merged.
|
||||
|
||||
Usage:
|
||||
python backlog_cleanup.py --repo Timmy_Foundation/timmy-home --dry-run
|
||||
python backlog_cleanup.py --repo Timmy_Foundation/timmy-home --close
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_token():
|
||||
f = Path.home() / ".config" / "gitea" / "token"
|
||||
if f.exists():
|
||||
return f.read_text().strip()
|
||||
return os.environ.get("GITEA_TOKEN", "")
|
||||
|
||||
|
||||
def api(base, token, path, method="GET", data=None):
|
||||
url = f"{base}/api/v1{path}"
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
body = json.dumps(data).encode() if data else None
|
||||
if data:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
return json.loads(urllib.request.urlopen(req, timeout=15).read())
|
||||
except Exception as e:
|
||||
print(f" API error: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--repo", default="Timmy_Foundation/timmy-home")
|
||||
p.add_argument("--base", default="https://forge.alexanderwhitestone.com")
|
||||
p.add_argument("--dry-run", action="store_true", default=True)
|
||||
p.add_argument("--close", action="store_true")
|
||||
p.add_argument("--limit", type=int, default=20)
|
||||
args = p.parse_args()
|
||||
if args.close:
|
||||
args.dry_run = False
|
||||
|
||||
token = get_token()
|
||||
issues = api(args.base, token, f"/repos/{args.repo}/issues?state=open&limit={args.limit}")
|
||||
if not issues:
|
||||
return 1
|
||||
|
||||
issues = [i for i in issues if not i.get("pull_request")]
|
||||
print(f"Scanning {len(issues)} issues...")
|
||||
|
||||
closable = []
|
||||
for issue in issues:
|
||||
if issue.get("assignees"):
|
||||
continue
|
||||
labels = {l.get("name", "").lower() for l in issue.get("labels", [])}
|
||||
if labels & {"epic", "in-progress", "claw-code-in-progress", "blocked"}:
|
||||
continue
|
||||
|
||||
# Check for merged PRs referencing this issue
|
||||
ref = f"#{issue['number']}"
|
||||
prs = api(args.base, token, f"/repos/{args.repo}/pulls?state=all&limit=20")
|
||||
time.sleep(0.1) # Rate limit
|
||||
|
||||
linked_merged = [
|
||||
pr for pr in (prs or [])
|
||||
if ref in (pr.get("body", "") + pr.get("title", ""))
|
||||
and (pr.get("state") == "merged" or pr.get("merged"))
|
||||
]
|
||||
|
||||
if linked_merged:
|
||||
reason = f"merged PR #{linked_merged[0]['number']}"
|
||||
closable.append((issue, reason))
|
||||
tag = "WOULD CLOSE" if args.dry_run else "CLOSING"
|
||||
print(f" {tag} #{issue['number']}: {issue['title'][:50]} — {reason}")
|
||||
|
||||
if not closable:
|
||||
print("No issues to close.")
|
||||
return 0
|
||||
|
||||
print(f"\n{'Would close' if args.dry_run else 'Closing'} {len(closable)} issues")
|
||||
if args.dry_run:
|
||||
print("(use --close to execute)")
|
||||
return 0
|
||||
|
||||
closed = 0
|
||||
for issue, reason in closable:
|
||||
api(args.base, token, f"/repos/{args.repo}/issues/{issue['number']}/comments",
|
||||
method="POST", data={"body": f"Closing — {reason}.\nAutomated by backlog_cleanup.py"})
|
||||
r = api(args.base, token, f"/repos/{args.repo}/issues/{issue['number']}",
|
||||
method="POST", data={"state": "closed"})
|
||||
if r:
|
||||
closed += 1
|
||||
print(f" Closed #{issue['number']}")
|
||||
time.sleep(0.2)
|
||||
|
||||
print(f"\nClosed {closed}/{len(closable)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,12 +6,6 @@ Local runtime target:
|
||||
Main commands:
|
||||
- `python3 scripts/evennia/bootstrap_local_evennia.py`
|
||||
- `python3 scripts/evennia/verify_local_evennia.py`
|
||||
- `python3 scripts/evennia/repair_evennia_vps.py --settings-path /root/wizards/bezalel/evennia/bezalel_world/server/conf/settings.py --game-dir /root/wizards/bezalel/evennia/bezalel_world --execute`
|
||||
|
||||
Bezalel VPS repair target from issue #534:
|
||||
- host: `104.131.15.18`
|
||||
- purpose: remove broken port tuple overrides (`WEBSERVER_PORTS`, `TELNET_PORTS`, `WEBSOCKET_PORTS`) and rewrite `SERVERNAME` only
|
||||
- the repair script prints recovery commands by default and can execute them when the Evennia runtime paths are correct
|
||||
|
||||
Hermes control path:
|
||||
- `scripts/evennia/evennia_mcp_server.py`
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
BAD_SETTING_KEYS = (
|
||||
"WEBSERVER_PORTS",
|
||||
"TELNET_PORTS",
|
||||
"WEBSOCKET_PORTS",
|
||||
"SERVERNAME",
|
||||
)
|
||||
|
||||
|
||||
def repair_settings_text(text: str, server_name: str = "bezalel_world") -> str:
|
||||
"""Remove broken port tuple overrides and rewrite SERVERNAME only."""
|
||||
kept: list[str] = []
|
||||
for line in text.splitlines():
|
||||
if any(key in line for key in BAD_SETTING_KEYS):
|
||||
continue
|
||||
kept.append(line)
|
||||
while kept and kept[-1] == "":
|
||||
kept.pop()
|
||||
kept.append(f'SERVERNAME = "{server_name}"')
|
||||
kept.append("")
|
||||
return "\n".join(kept)
|
||||
|
||||
|
||||
def repair_settings_file(path: Path, server_name: str = "bezalel_world") -> str:
|
||||
original = path.read_text()
|
||||
repaired = repair_settings_text(original, server_name=server_name)
|
||||
path.write_text(repaired)
|
||||
return repaired
|
||||
|
||||
|
||||
def build_superuser_python(game_dir: str, username: str, email: str, password: str) -> str:
|
||||
game_dir_q = repr(game_dir)
|
||||
username_q = repr(username)
|
||||
email_q = repr(email)
|
||||
password_q = repr(password)
|
||||
return f"""import os, sys
|
||||
sys.setrecursionlimit(5000)
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'server.conf.settings'
|
||||
os.chdir({game_dir_q})
|
||||
import django
|
||||
django.setup()
|
||||
from evennia.accounts.accounts import AccountDB
|
||||
if not AccountDB.objects.filter(username={username_q}).exists():
|
||||
AccountDB.objects.create_superuser({username_q}, {email_q}, {password_q})
|
||||
print('SUPERUSER_OK')
|
||||
"""
|
||||
|
||||
|
||||
def build_recovery_commands(
|
||||
game_dir: str,
|
||||
evennia_bin: str,
|
||||
python_bin: str,
|
||||
username: str = "Timmy",
|
||||
email: str = "timmy@tower.world",
|
||||
password: str = "timmy123",
|
||||
) -> list[str]:
|
||||
quoted_game = shlex.quote(game_dir)
|
||||
quoted_evennia = shlex.quote(evennia_bin)
|
||||
quoted_python = shlex.quote(python_bin)
|
||||
superuser_code = build_superuser_python(game_dir, username, email, password)
|
||||
superuser_cmd = f"{quoted_python} -c {shlex.quote(superuser_code)}"
|
||||
return [
|
||||
f"cd {quoted_game}",
|
||||
"rm -f server/evennia.db3",
|
||||
f"{quoted_evennia} migrate",
|
||||
superuser_cmd,
|
||||
f"{quoted_evennia} start",
|
||||
f"{quoted_evennia} status",
|
||||
]
|
||||
|
||||
|
||||
def execute(commands: list[str]) -> int:
|
||||
shell = "set -euo pipefail\n" + "\n".join(commands)
|
||||
return subprocess.run(["bash", "-lc", shell], check=False).returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Repair an Evennia VPS settings file and print/apply recovery commands.")
|
||||
parser.add_argument("--settings-path", default="/root/wizards/bezalel/evennia/bezalel_world/server/conf/settings.py")
|
||||
parser.add_argument("--game-dir", default="/root/wizards/bezalel/evennia/bezalel_world")
|
||||
parser.add_argument("--evennia-bin", default="/root/wizards/bezalel/evennia/venv/bin/evennia")
|
||||
parser.add_argument("--python-bin", default="/root/wizards/bezalel/evennia/venv/bin/python3")
|
||||
parser.add_argument("--server-name", default="bezalel_world")
|
||||
parser.add_argument("--username", default="Timmy")
|
||||
parser.add_argument("--email", default="timmy@tower.world")
|
||||
parser.add_argument("--password", default="timmy123")
|
||||
parser.add_argument("--execute", action="store_true", help="Apply settings and run recovery commands instead of printing them.")
|
||||
args = parser.parse_args()
|
||||
|
||||
settings_path = Path(args.settings_path)
|
||||
if args.execute:
|
||||
repair_settings_file(settings_path, server_name=args.server_name)
|
||||
else:
|
||||
print(f"# Would rewrite {settings_path} to remove broken port tuple overrides")
|
||||
if settings_path.exists():
|
||||
print(repair_settings_text(settings_path.read_text(), server_name=args.server_name))
|
||||
else:
|
||||
print(f"# Settings file not found: {settings_path}")
|
||||
|
||||
commands = build_recovery_commands(
|
||||
game_dir=args.game_dir,
|
||||
evennia_bin=args.evennia_bin,
|
||||
python_bin=args.python_bin,
|
||||
username=args.username,
|
||||
email=args.email,
|
||||
password=args.password,
|
||||
)
|
||||
|
||||
if args.execute:
|
||||
return execute(commands)
|
||||
|
||||
print("# Recovery commands")
|
||||
print("\n".join(commands))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,48 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.evennia.repair_evennia_vps import build_recovery_commands, repair_settings_text
|
||||
|
||||
|
||||
SCRIPT = Path("scripts/evennia/repair_evennia_vps.py")
|
||||
README = Path("scripts/evennia/README.md")
|
||||
|
||||
|
||||
def test_repair_script_exists() -> None:
|
||||
assert SCRIPT.exists()
|
||||
|
||||
|
||||
def test_repair_settings_text_removes_bad_port_tuple_overrides() -> None:
|
||||
original = """# settings\nSERVERNAME = \"old\"\nWEBSERVER_PORTS = [(4101, None)]\nTELNET_PORTS = [(4000, 4001)]\nWEBSOCKET_PORTS = [(4102, None)]\nDEBUG = False\n"""
|
||||
|
||||
repaired = repair_settings_text(original, server_name="bezalel_world")
|
||||
|
||||
assert 'WEBSERVER_PORTS' not in repaired
|
||||
assert 'TELNET_PORTS' not in repaired
|
||||
assert 'WEBSOCKET_PORTS' not in repaired
|
||||
assert 'SERVERNAME = "old"' not in repaired
|
||||
assert 'SERVERNAME = "bezalel_world"' in repaired
|
||||
assert 'DEBUG = False' in repaired
|
||||
|
||||
|
||||
def test_build_recovery_commands_contains_evennia_recovery_steps() -> None:
|
||||
commands = build_recovery_commands(
|
||||
game_dir="/root/wizards/bezalel/evennia/bezalel_world",
|
||||
evennia_bin="/root/wizards/bezalel/evennia/venv/bin/evennia",
|
||||
python_bin="/root/wizards/bezalel/evennia/venv/bin/python3",
|
||||
username="Timmy",
|
||||
email="timmy@tower.world",
|
||||
password="timmy123",
|
||||
)
|
||||
|
||||
joined = "\n".join(commands)
|
||||
assert "rm -f server/evennia.db3" in joined
|
||||
assert "evennia migrate" in joined
|
||||
assert "create_superuser" in joined
|
||||
assert "evennia start" in joined
|
||||
assert "evennia status" in joined
|
||||
|
||||
|
||||
def test_evennia_readme_mentions_repair_script() -> None:
|
||||
content = README.read_text()
|
||||
assert "repair_evennia_vps.py" in content
|
||||
assert "104.131.15.18" in content
|
||||
Reference in New Issue
Block a user