diff --git a/.github/workflows/ai-pr-summary.yml b/.github/workflows/ai-pr-summary.yml new file mode 100644 index 0000000..3172969 --- /dev/null +++ b/.github/workflows/ai-pr-summary.yml @@ -0,0 +1,124 @@ +name: PR AI Summary + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + summarize: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Get PR diff + id: diff + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + # Trae exactamente esos commits (evita problemas de merge-base y shallow clones) + git fetch --no-tags --prune --depth=1 origin $BASE $HEAD + git diff $BASE $HEAD > pr.diff + echo "path=pr.diff" >> $GITHUB_OUTPUT + + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install deps + run: | + python -m pip install --upgrade pip + pip install openai==1.* # SDK oficial + + - name: Generate AI summary (OpenAI) + id: ai + continue-on-error: true + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + MODEL: gpt-4o-mini + run: | + python - << 'PY' + import os + from openai import OpenAI + client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + + with open("pr.diff","r",encoding="utf-8") as f: + diff = f.read()[:200000] # tope por costos/ruido + + prompt = ( + "You are a code reviewer. Summarize this PR in 2-20 bullets. " + "Include WHAT changed, WHY it matters, RISKS, TESTS to add, and any BREAKING CHANGES. " + "Highlight key fetures or changes. Consider markdown as the default output format." + "Keep it concise and actionable.\n\nDIFF:\n" + diff + ) + + resp = client.chat.completions.create( + model=os.getenv("MODEL","gpt-4o-mini"), + temperature=0.2, + messages=[{"role":"user","content":prompt}], + ) + text = resp.choices[0].message.content.strip() + with open("summary.txt","w",encoding="utf-8") as f: + f.write(text) + PY + + - name: Heuristic fallback if AI failed + if: ${{ steps.ai.outcome == 'failure' }} + run: | + python - << 'PY' + import re, pathlib + diff = pathlib.Path("pr.diff").read_text(encoding="utf-8") + + added = len(re.findall(r"^\\+[^+].*$", diff, flags=re.M)) + removed = len(re.findall(r"^\\-[^-].*$", diff, flags=re.M)) + files = re.findall(r"^\\+\\+\\+ b/(.+)$", diff, flags=re.M) + + scopes = set() + for f in files: + fl = f.lower() + if "/controller" in fl: scopes.add("controller") + elif "/service" in fl: scopes.add("service") + elif "/repository" in fl or "jparepository" in diff.lower(): scopes.add("repository") + elif "/entity" in fl or "/model" in fl: scopes.add("entity") + elif "application" in fl and (fl.endswith(".yml") or fl.endswith(".yaml") or fl.endswith(".properties")): + scopes.add("config") + elif fl.endswith("test.java"): scopes.add("test") + + scope = ",".join(sorted(scopes)) if scopes else "core" + kind = "refactor" + if added and not removed: kind = "feat" + if removed and not added: kind = "chore" + if re.search(r"@Test", diff): kind = "test" + if re.search(r"fix|bug|exception|stacktrace", diff, re.I): kind = "fix" + + subject = f"[Fallback] {kind}({scope}): {len(files)} file(s), +{added}/-{removed}" + + bullets = [] + bullets.append(f"- Files changed: {len(files)}") + bullets.append(f"- Lines: +{added} / -{removed}") + if scopes: + bullets.append(f"- Layers: {', '.join(sorted(scopes))}") + if re.search(r"@Transactional", diff): bullets.append("- Touches transactional boundaries") + if re.search(r"@RestController|@Controller", diff): bullets.append("- Controller changes present") + if re.search(r"@Service", diff): bullets.append("- Service-layer changes present") + if re.search(r"@Repository|JpaRepository", diff): bullets.append("- Repository-layer changes present") + if re.search(r"todo|fixme", diff, re.I): bullets.append("- Contains TODO/FIXME markers") + + text = subject + "\\n\\n" + "\\n".join(bullets) + pathlib.Path("summary.txt").write_text(text, encoding="utf-8") + PY + + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: ai-pr-summary + recreate: true + path: summary.txt +