1+ name : PR AI Summary
2+
3+ on :
4+ pull_request :
5+ types : [opened, synchronize, reopened]
6+
7+ permissions :
8+ contents : read
9+ pull-requests : write
10+
11+ jobs :
12+ summarize :
13+ runs-on : ubuntu-latest
14+
15+ steps :
16+ - uses : actions/checkout@v4
17+
18+ - name : Get PR diff
19+ id : diff
20+ run : |
21+ BASE="${{ github.event.pull_request.base.sha }}"
22+ HEAD="${{ github.event.pull_request.head.sha }}"
23+ # Trae exactamente esos commits (evita problemas de merge-base y shallow clones)
24+ git fetch --no-tags --prune --depth=1 origin $BASE $HEAD
25+ git diff $BASE $HEAD > pr.diff
26+ echo "path=pr.diff" >> $GITHUB_OUTPUT
27+
28+ - name : Set up Python
29+ uses : actions/setup-python@v5
30+ with :
31+ python-version : ' 3.11'
32+
33+ - name : Install deps
34+ run : |
35+ python -m pip install --upgrade pip
36+ pip install openai==1.* # SDK oficial
37+
38+ - name : Generate AI summary (OpenAI)
39+ id : ai
40+ continue-on-error : true
41+ env :
42+ OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
43+ MODEL : gpt-4o-mini
44+ run : |
45+ python - << 'PY'
46+ import os
47+ from openai import OpenAI
48+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
49+
50+ with open("pr.diff","r",encoding="utf-8") as f:
51+ diff = f.read()[:200000] # tope por costos/ruido
52+
53+ prompt = (
54+ "You are a code reviewer. Summarize this PR in 2-20 bullets. "
55+ "Include WHAT changed, WHY it matters, RISKS, TESTS to add, and any BREAKING CHANGES. "
56+ "Highlight key features or changes. Consider markdown as the default output format."
57+ "Keep in mind the following points:"
58+ "1) If DIFF shows only documentation files (e.g., .md/.mdx/.txt/README), state 'Docs-only change', "
59+ " make clear that the change is included only in documentation files, if that is the case, "
60+ " otherwise explain normally, considering the DIFF changes like normal. "
61+ "2) Include a short list of changed file paths as extracted from DIFF. "
62+ "Keep it concise and actionable.\n\nDIFF:\n" + diff
63+ )
64+
65+ resp = client.chat.completions.create(
66+ model=os.getenv("MODEL","gpt-4o-mini"),
67+ temperature=0.2,
68+ messages=[{"role":"user","content":prompt}],
69+ )
70+ text = resp.choices[0].message.content.strip()
71+ with open("summary.txt","w",encoding="utf-8") as f:
72+ f.write(text)
73+ PY
74+
75+ - name : Heuristic fallback if AI failed
76+ if : ${{ steps.ai.outcome == 'failure' }}
77+ run : |
78+ python - << 'PY'
79+ import re, pathlib
80+ diff = pathlib.Path("pr.diff").read_text(encoding="utf-8")
81+
82+ added = len(re.findall(r"^\\+[^+].*$", diff, flags=re.M))
83+ removed = len(re.findall(r"^\\-[^-].*$", diff, flags=re.M))
84+ files = re.findall(r"^\\+\\+\\+ b/(.+)$", diff, flags=re.M)
85+
86+ lower_paths = [f.lower() for f in files]
87+ DOC_EXT = (".md", ".mdx", ".txt", ".rst", ".adoc")
88+ is_doc = lambda p: p.endswith(DOC_EXT) or "/docs/" in p or "/doc/" in p
89+ docs_only = len(files) > 0 and all(is_doc(p) for p in lower_paths)
90+
91+ # ---------- Doc-only summary ----------
92+ if docs_only:
93+ bullets_changed = []
94+ for f in files[:20]: # evita listas enormes
95+ bullets_changed.append(f"- `{f}`")
96+ doc_summary = [
97+ "## PR Summary",
98+ "",
99+ "### WHAT Changed",
100+ "- **Docs-only change** detected from DIFF.",
101+ f"- Files changed ({len(files)}):",
102+ *bullets_changed,
103+ "",
104+ "### WHY It Matters",
105+ "- Improves documentation/README clarity and onboarding experience.",
106+ "",
107+ "### RISKS",
108+ "- None to runtime behavior (documentation only).",
109+ "",
110+ "### TESTS to Add",
111+ "- N/A (no code changes).",
112+ "",
113+ "### BREAKING CHANGES",
114+ "- None.",
115+ ]
116+ pathlib.Path("summary.txt").write_text("\n".join(doc_summary), encoding="utf-8")
117+ raise SystemExit(0)
118+
119+ scopes = set()
120+ for f in files:
121+ fl = f.lower()
122+ if "/controller" in fl: scopes.add("controller")
123+ elif "/service" in fl: scopes.add("service")
124+ elif "/repository" in fl or "jparepository" in diff.lower(): scopes.add("repository")
125+ elif "/entity" in fl or "/model" in fl: scopes.add("entity")
126+ elif "application" in fl and (fl.endswith(".yml") or fl.endswith(".yaml") or fl.endswith(".properties")):
127+ scopes.add("config")
128+ elif fl.endswith("test.java"): scopes.add("test")
129+
130+ scope = ",".join(sorted(scopes)) if scopes else "core"
131+ kind = "refactor"
132+ if added and not removed: kind = "feat"
133+ if removed and not added: kind = "chore"
134+ if re.search(r"@Test", diff): kind = "test"
135+ if re.search(r"fix|bug|exception|stacktrace", diff, re.I): kind = "fix"
136+
137+ subject = f"[Fallback] {kind}({scope}): {len(files)} file(s), +{added}/-{removed}"
138+
139+ bullets = []
140+ bullets.append(f"- Files changed: {len(files)}")
141+ bullets.append(f"- Lines: +{added} / -{removed}")
142+ if scopes:
143+ bullets.append(f"- Layers: {', '.join(sorted(scopes))}")
144+ if re.search(r"@Transactional", diff): bullets.append("- Touches transactional boundaries")
145+ if re.search(r"@RestController|@Controller", diff): bullets.append("- Controller changes present")
146+ if re.search(r"@Service", diff): bullets.append("- Service-layer changes present")
147+ if re.search(r"@Repository|JpaRepository", diff): bullets.append("- Repository-layer changes present")
148+ if re.search(r"todo|fixme", diff, re.I): bullets.append("- Contains TODO/FIXME markers")
149+
150+ text = subject + "\\n\\n" + "\\n".join(bullets)
151+ pathlib.Path("summary.txt").write_text(text, encoding="utf-8")
152+ PY
153+
154+ - name : Comment on PR
155+ uses : marocchino/sticky-pull-request-comment@v2
156+ with :
157+ header : ai-pr-summary
158+ recreate : true
159+ path : summary.txt
0 commit comments