Skip to content

Commit 8033669

Browse files
committed
feat: suggest documentation updates during review
1 parent 2ff626a commit 8033669

3 files changed

Lines changed: 157 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ To submit merge request into review run command:
124124
gh review
125125
```
126126

127+
If `OPENAI_API_KEY` is configured, the review command also checks the diff for
128+
documentation impact and posts suggested documentation updates to the merge
129+
request when user-facing behavior, setup, configuration, or command usage
130+
changes.
131+
127132
To also enable **auto-merge when the pipeline succeeds**, add `--auto_merge` or `-am` flag:
128133

129134
```
@@ -225,4 +230,3 @@ I suggest checking Gitlab's official API documentation: https://docs.gitlab.com/
225230
## Donating 💜
226231

227232
Make sure to check this project on [OpenPledge](https://app.openpledge.io/repositories/zigcBenx/gitHappens).
228-

ai_code_review.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,35 @@ class Colors:
4545
- MEDIUM: Code smells, potential bugs, missing error handling
4646
- LOW: Minor improvements, suggestions, style inconsistencies"""
4747

48+
DOCUMENTATION_PROMPT = """Documentation review: you are a technical writer reviewing a git diff for documentation impact.
49+
50+
Output ONLY valid JSON - no markdown, no code blocks, no explanations.
51+
52+
Identify whether the diff changes user-facing behavior, setup steps, configuration,
53+
commands, flags, public APIs, or operational workflows that should be documented.
54+
Do not suggest documentation for purely internal refactors, test-only changes, or
55+
minor implementation details that users do not need to know.
56+
57+
Output format:
58+
{
59+
"needed": true,
60+
"summary": "one sentence describing why docs should change",
61+
"suggestions": [
62+
{
63+
"file": "README.md",
64+
"reason": "what changed in the diff",
65+
"suggestion": "specific documentation update to make"
66+
}
67+
]
68+
}
69+
70+
If no documentation update is needed, return:
71+
{
72+
"needed": false,
73+
"summary": "No documentation updates needed.",
74+
"suggestions": []
75+
}"""
76+
4877
def get_branch_diff():
4978
"""Get the diff of changed files in current branch vs main branch."""
5079
try:
@@ -130,6 +159,32 @@ def review_code(diff_content):
130159
print(f"{Colors.CRITICAL}✗ Error during AI review: {e}{Colors.RESET}")
131160
return None
132161

162+
def suggest_documentation_updates(diff_content):
163+
"""Send code diff to OpenAI for documentation update suggestions."""
164+
openai = get_openai_client()
165+
if not openai:
166+
return None
167+
168+
try:
169+
response = openai.chat.completions.create(
170+
model="gpt-4o",
171+
messages=[
172+
{"role": "system", "content": DOCUMENTATION_PROMPT},
173+
{"role": "user", "content": f"Documentation review for this git diff:\n\n{diff_content}"}
174+
],
175+
temperature=0.2,
176+
response_format={"type": "json_object"}
177+
)
178+
179+
return json.loads(response.choices[0].message.content)
180+
except json.JSONDecodeError as e:
181+
print(f"{Colors.CRITICAL}✗ Failed to parse documentation response as JSON{Colors.RESET}")
182+
print(f"{Colors.DIM}Error: {e}{Colors.RESET}")
183+
return None
184+
except Exception as e:
185+
print(f"{Colors.CRITICAL}✗ Error during documentation review: {e}{Colors.RESET}")
186+
return None
187+
133188
def print_issues(issues, severity, color, icon):
134189
"""Print issues with consistent formatting."""
135190
if not issues:
@@ -213,6 +268,36 @@ def format_issues(issues, severity, emoji):
213268

214269
return comment
215270

271+
def format_documentation_comment(results):
272+
"""Format documentation suggestions as a GitLab markdown comment."""
273+
if not results or not results.get('needed') or not results.get('suggestions'):
274+
return None
275+
276+
comment = "## Documentation Suggestions\n\n"
277+
278+
summary = results.get('summary', '')
279+
if summary:
280+
comment += f"{summary}\n\n"
281+
282+
for suggestion in results.get('suggestions', []):
283+
file_path = suggestion.get('file', 'Documentation')
284+
reason = suggestion.get('reason', 'Documentation may need an update.')
285+
update = suggestion.get('suggestion', 'Review the diff and update documentation as needed.')
286+
comment += f"- **`{file_path}`**: {update}\n"
287+
comment += f" - Reason: {reason}\n"
288+
289+
return comment
290+
291+
def display_documentation_results(results):
292+
"""Display documentation suggestions in the terminal."""
293+
comment = format_documentation_comment(results)
294+
if not comment:
295+
print(f"{Colors.INFO}ℹ No documentation updates suggested{Colors.RESET}")
296+
return
297+
298+
print(f"\n{Colors.BOLD}DOCUMENTATION SUGGESTIONS{Colors.RESET}")
299+
print(comment)
300+
216301
def get_merge_request_changes(project_id, mr_id, gitlab_token, api_url):
217302
"""Get the changes (diffs) from the merge request to find commit SHAs."""
218303
import requests
@@ -340,6 +425,11 @@ def run_review():
340425
sys.exit(0)
341426
display_review_results(results)
342427

428+
print(f"{Colors.INFO}📝 Checking documentation impact...{Colors.RESET}")
429+
documentation_results = suggest_documentation_updates(diff_content)
430+
if documentation_results:
431+
display_documentation_results(documentation_results)
432+
343433
def run_review_for_mr(project_id, mr_id, gitlab_token, api_url):
344434
"""Run AI code review and post inline comments to GitLab merge request."""
345435
print(f"{Colors.INFO}🤖 Running AI code review...{Colors.RESET}")
@@ -353,6 +443,8 @@ def run_review_for_mr(project_id, mr_id, gitlab_token, api_url):
353443
print(f"{Colors.HIGH}⚠ AI review skipped{Colors.RESET}")
354444
return
355445

446+
documentation_results = suggest_documentation_updates(diff_content)
447+
356448
# Get diff refs for inline comments
357449
diff_refs = get_diff_refs(project_id, mr_id, gitlab_token, api_url)
358450
if not diff_refs or not all(diff_refs.values()):
@@ -386,5 +478,9 @@ def run_review_for_mr(project_id, mr_id, gitlab_token, api_url):
386478
else:
387479
print(f"{Colors.INFO}✓ All {total_posted} issues posted as inline comments{Colors.RESET}")
388480

481+
documentation_comment = format_documentation_comment(documentation_results)
482+
if documentation_comment:
483+
post_to_merge_request(documentation_comment, project_id, mr_id, gitlab_token, api_url)
484+
389485
if __name__ == '__main__':
390486
run_review()

tests/test_ai_code_review.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import json
2+
import unittest
3+
from unittest import mock
4+
5+
import ai_code_review
6+
7+
8+
class DocumentationSuggestionTest(unittest.TestCase):
9+
def test_format_documentation_comment_returns_none_without_suggestions(self):
10+
result = ai_code_review.format_documentation_comment({
11+
"needed": False,
12+
"summary": "No docs needed",
13+
"suggestions": [],
14+
})
15+
16+
self.assertIsNone(result)
17+
18+
def test_format_documentation_comment_includes_summary_and_suggestions(self):
19+
result = ai_code_review.format_documentation_comment({
20+
"needed": True,
21+
"summary": "The CLI behavior changed.",
22+
"suggestions": [
23+
{
24+
"file": "README.md",
25+
"reason": "New flag added",
26+
"suggestion": "Document the --select flag in the review section.",
27+
}
28+
],
29+
})
30+
31+
self.assertIn("## Documentation Suggestions", result)
32+
self.assertIn("The CLI behavior changed.", result)
33+
self.assertIn("README.md", result)
34+
self.assertIn("Document the --select flag", result)
35+
36+
def test_suggest_documentation_updates_uses_diff_content(self):
37+
fake_openai = mock.Mock()
38+
fake_openai.chat.completions.create.return_value.choices = [
39+
mock.Mock(message=mock.Mock(content=json.dumps({
40+
"needed": True,
41+
"summary": "Docs should mention behavior.",
42+
"suggestions": [],
43+
})))
44+
]
45+
46+
with mock.patch("ai_code_review.get_openai_client", return_value=fake_openai):
47+
result = ai_code_review.suggest_documentation_updates("diff --git a/file.py b/file.py")
48+
49+
self.assertTrue(result["needed"])
50+
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
51+
self.assertIn("Documentation", call_kwargs["messages"][0]["content"])
52+
self.assertIn("diff --git", call_kwargs["messages"][1]["content"])
53+
54+
55+
if __name__ == "__main__":
56+
unittest.main()

0 commit comments

Comments
 (0)