-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathexplain_commit.py
More file actions
166 lines (138 loc) · 6.45 KB
/
Copy pathexplain_commit.py
File metadata and controls
166 lines (138 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
"""Generate an AI summary of the latest commit via Gemini and emit title/body for a GitHub Release."""
import json
import os
import re
import subprocess
import sys
import time
import urllib.request
MODEL = "gemini-2.5-flash"
MAX_DIFF_CHARS = 600_000
def run(cmd: list[str]) -> str:
return subprocess.check_output(cmd, text=True, encoding="utf-8", errors="replace")
def build_context(sha: str) -> tuple[str, bool]:
stat = run(["git", "show", "--stat", "--format=%H%n%s%n%an%n%ad", sha])
new_files = run(["git", "diff", f"{sha}~1", sha, "--diff-filter=A", "--name-only"])
diff = run(["git", "diff", "-U1", f"{sha}~1", sha])
base = "## git show --stat:\n" + stat + "\n## New files:\n" + new_files
if len(diff) <= MAX_DIFF_CHARS:
return base + "\n## Diff (-U1):\n" + diff, True
return base, False
def call_gemini(api_key: str, context: str, has_diff: bool) -> dict:
if has_diff:
guidance = (
"You have the FULL diff. Be technical and ALWAYS include code evidence.\n"
"For EACH bullet point you MUST include at least one short ```js snippet (1-3 lines) "
"showing a new function, identifier, string literal, or config value from the diff.\n"
"Pick the most revealing line — e.g. a new export, a feature gate string, a new enum value.\n"
"Ignore pure minification noise (variable renames, module ID shuffles)."
)
else:
guidance = (
"Diff was too large to include — infer from FILENAMES and --stat only. "
"No code snippets; describe themes."
)
prompt = (
"You summarize commits from a repo that stores daily dumps of the WhatsApp Web bundle "
f"(transpiled/minified code). {guidance}\n\n"
"CRITICAL: Your response MUST be valid JSON with EXACTLY these 4 keys — no more, no less:\n"
'{"title_en": "...", "body_en": "...", "title_pt": "...", "body_pt": "..."}\n\n'
"ALL 4 keys are REQUIRED. Do NOT omit any key. Do NOT use other key names.\n\n"
"- title_en: up to 80 characters, English, highlights the 1-2 main themes.\n"
"- title_pt: up to 80 characters, Brazilian Portuguese translation of title_en.\n"
"- body_en: markdown, English, bullets grouped by theme "
"(e.g. **AI Reply Bot**, **Polls**, **Newsletter**). Short, no preamble. Max 12 bullets.\n"
"- body_pt: markdown, Brazilian Portuguese translation of body_en. "
"Keep code snippets and identifiers identical — translate only prose.\n\n"
f"{context}"
)
payload = json.dumps({
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"responseMimeType": "application/json"},
}).encode()
url = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent?key={api_key}"
for attempt in range(1, 4):
try:
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
retryable = e.code == 429 or e.code >= 500
if attempt == 3 or not retryable:
raise
delay = attempt * 30
print(f"Gemini returned {e.code}, retrying in {delay}s...")
time.sleep(delay)
continue
text = data["candidates"][0]["content"]["parts"][0]["text"]
parsed = json.loads(text)
if all(k in parsed for k in ("title_en", "body_en", "title_pt", "body_pt")):
return {
"title_en": parsed["title_en"].strip(),
"body_en": parsed["body_en"].strip(),
"title_pt": parsed["title_pt"].strip(),
"body_pt": parsed["body_pt"].strip(),
}
print(f"Invalid output (attempt {attempt}): {text[:200]}")
time.sleep(attempt * 10)
raise RuntimeError("Gemini failed to return valid output after 3 attempts")
def fix_code_fences(md: str) -> str:
"""Re-indent fenced code blocks so their content keeps the list indentation.
Gemini often emits code lines at column 0 inside fences opened within
indented list items, which terminates the list mid-fence in CommonMark
and mangles the rest of the document.
"""
out = []
fence_indent = None
for line in md.splitlines():
stripped = line.lstrip()
if fence_indent is None:
if stripped.startswith("```"):
fence_indent = line[: len(line) - len(stripped)]
out.append(line)
elif stripped.startswith("```"):
out.append(fence_indent + "```")
fence_indent = None
else:
out.append(line if line.startswith(fence_indent) or not stripped else fence_indent + line)
return "\n".join(out)
def emit_outputs(title: str, body: str, tag: str) -> None:
out_path = os.environ.get("GITHUB_OUTPUT")
if not out_path:
sys.stdout.buffer.write(f"title={title}\ntag={tag}\n\n{body}\n".encode("utf-8"))
return
with open(out_path, "a", encoding="utf-8") as f:
f.write(f"title={title}\n")
f.write(f"tag={tag}\n")
with open("release_body.md", "w", encoding="utf-8") as f:
f.write(body)
def main() -> int:
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
print("GEMINI_API_KEY missing", file=sys.stderr)
return 1
sha = run(["git", "rev-parse", "HEAD"]).strip()
commit_date = run(["git", "show", "-s", "--format=%cd", "--date=format:%Y.%m.%d-%H%M", sha]).strip()
tag = f"v{commit_date}"
context, has_diff = build_context(sha)
result = call_gemini(api_key, context, has_diff)
repo = os.environ.get("GITHUB_REPOSITORY", "")
server = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
commit_link = f"[`{sha[:10]}`]({server}/{repo}/commit/{sha})" if repo else f"`{sha[:10]}`"
body_en = fix_code_fences(result["body_en"])
body_pt = fix_code_fences(result["body_pt"])
body = (
"<details open><summary>🇺🇸 <b>English</b></summary>\n\n"
f"{body_en}\n\n"
"</details>\n\n"
"<details><summary>🇧🇷 <b>Português</b></summary>\n\n"
f"{body_pt}\n\n"
"</details>\n\n"
f"---\nCommit: {commit_link}"
)
title = re.sub(r"\s+", " ", result["title_en"])[:80]
emit_outputs(title, body, tag)
return 0
if __name__ == "__main__":
sys.exit(main())