Files
homelab-optimized/scripts/changelog-generator.py
Gitea Mirror Bot 082633dad9
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-04-05 10:50:43 UTC
2026-04-05 10:50:43 +00:00

144 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Generate changelogs from git commits using Ollama LLM.
Reads recent git commits, groups them via LLM into Features/Fixes/Infrastructure/Documentation,
and prepends the result to docs/CHANGELOG.md.
Cron: Monday 8am
0 8 * * 1 cd /home/homelab/organized/repos/homelab && python3 scripts/changelog-generator.py
"""
import argparse
import logging
import sqlite3
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from lib.ollama import ollama_generate, ollama_available, OllamaUnavailableError
log = logging.getLogger(__name__)
REPO_DIR = Path("/home/homelab/organized/repos/homelab")
CHANGELOG_PATH = REPO_DIR / "docs" / "CHANGELOG.md"
DB_PATH = Path(__file__).parent / "changelog.db"
def init_db(db_path: Path) -> sqlite3.Connection:
"""Initialise the SQLite DB and return a connection."""
conn = sqlite3.connect(str(db_path))
conn.execute(
"CREATE TABLE IF NOT EXISTS runs "
"(id INTEGER PRIMARY KEY AUTOINCREMENT, last_commit TEXT, run_at TEXT)"
)
conn.commit()
return conn
def get_last_commit_date(conn: sqlite3.Connection) -> str | None:
"""Return the run_at timestamp of the most recent run, or None."""
row = conn.execute("SELECT run_at FROM runs ORDER BY id DESC LIMIT 1").fetchone()
return row[0] if row else None
def fetch_commits(since: str) -> list[str]:
"""Return git log --oneline entries since *since* (ISO date string)."""
result = subprocess.run(
["git", "-C", str(REPO_DIR), "log", "--oneline", f"--since={since}", "main"],
capture_output=True,
text=True,
)
if result.returncode != 0:
log.error("git log failed: %s", result.stderr.strip())
return []
lines = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
return lines
def generate_changelog(commits: list[str]) -> str:
"""Send commits to Ollama and return a formatted changelog."""
prompt = (
"Summarize these git commits into a changelog grouped by: "
"Features, Fixes, Infrastructure, Documentation. "
"Use bullet points. Be concise. Omit any empty groups.\n\n"
+ "\n".join(commits)
)
return ollama_generate(prompt, num_predict=1500)
def prepend_changelog(content: str, date_header: str) -> None:
"""Prepend a dated changelog entry to CHANGELOG.md."""
CHANGELOG_PATH.parent.mkdir(parents=True, exist_ok=True)
existing = CHANGELOG_PATH.read_text() if CHANGELOG_PATH.exists() else ""
entry = f"## {date_header}\n\n{content}\n\n---\n\n"
CHANGELOG_PATH.write_text(entry + existing)
log.info("Prepended changelog entry to %s", CHANGELOG_PATH)
def record_run(conn: sqlite3.Connection, last_commit: str) -> None:
"""Record the run in the DB."""
conn.execute(
"INSERT INTO runs (last_commit, run_at) VALUES (?, ?)",
(last_commit, datetime.now().isoformat()),
)
conn.commit()
def main() -> None:
parser = argparse.ArgumentParser(description="Generate changelog from git commits via LLM")
parser.add_argument("--dry-run", action="store_true", help="Print changelog without writing")
parser.add_argument("--since", type=str, help="Override start date (YYYY-MM-DD)")
parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
if not ollama_available():
log.error("Ollama is not reachable — aborting")
sys.exit(1)
conn = init_db(DB_PATH)
# Determine the since date
if args.since:
since = args.since
else:
last_run = get_last_commit_date(conn)
if last_run:
since = last_run[:10] # YYYY-MM-DD
else:
since = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
log.info("Fetching commits since %s", since)
commits = fetch_commits(since)
if not commits:
log.info("No new commits since %s — nothing to do", since)
return
log.info("Found %d commits, generating changelog...", len(commits))
changelog = generate_changelog(commits)
date_header = datetime.now().strftime("%Y-%m-%d")
if args.dry_run:
print(f"## {date_header}\n")
print(changelog)
return
prepend_changelog(changelog, date_header)
# Record the first commit hash (most recent) in the DB
first_hash = commits[0].split()[0]
record_run(conn, first_hash)
log.info("Done — recorded commit %s", first_hash)
if __name__ == "__main__":
main()