cicaddy-github 0.3.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.claude/skills/cicaddy-action/SKILL.md +1 -1
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/Dockerfile +1 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/PKG-INFO +4 -3
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/README.md +3 -2
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/action.yml +4 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/entrypoint.sh +10 -1
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/pyproject.toml +1 -1
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/config/settings.py +10 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/agents.py +80 -2
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/analyzer.py +17 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/security/leak_detector.py +1 -1
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_agents.py +40 -2
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_analyzer.py +43 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_settings.py +28 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.github/dependabot.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.github/workflows/changelog.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.github/workflows/ci.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.github/workflows/pr-review.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.github/workflows/release.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.gitignore +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/.pre-commit-config.yaml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/CLAUDE.md +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/CONTRIBUTING.md +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/LICENSE +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/config/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/detector.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/tools.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/plugin.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/security/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/validation.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tasks/changelog_report.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tasks/pr_review.yml +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/templates/report_template.html +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/conftest.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/__init__.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_detector.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_leak_detector.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_plugin.py +0 -0
- {cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/tests/unit/test_tools.py +0 -0
|
@@ -225,7 +225,7 @@ the `safe-to-review` label. The label is auto-removed on new pushes to prevent
|
|
|
225
225
|
TOCTOU bypasses.
|
|
226
226
|
|
|
227
227
|
```yaml
|
|
228
|
-
- uses: redhat-community-ai-tools/cicaddy-action@v0.
|
|
228
|
+
- uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
|
|
229
229
|
with:
|
|
230
230
|
ai_provider: gemini
|
|
231
231
|
ai_model: gemini-3-flash-preview
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cicaddy-github
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: GitHub Actions plugin for cicaddy AI agent framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/redhat-community-ai-tools/cicaddy-action
|
|
6
6
|
Project-URL: Repository, https://github.com/redhat-community-ai-tools/cicaddy-action.git
|
|
@@ -57,7 +57,7 @@ jobs:
|
|
|
57
57
|
with:
|
|
58
58
|
fetch-depth: 0
|
|
59
59
|
|
|
60
|
-
- uses: redhat-community-ai-tools/cicaddy-action@v0.
|
|
60
|
+
- uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
|
|
61
61
|
with:
|
|
62
62
|
ai_provider: gemini
|
|
63
63
|
ai_model: gemini-3-flash-preview
|
|
@@ -83,7 +83,7 @@ jobs:
|
|
|
83
83
|
with:
|
|
84
84
|
fetch-depth: 0
|
|
85
85
|
|
|
86
|
-
- uses: redhat-community-ai-tools/cicaddy-action@v0.
|
|
86
|
+
- uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
|
|
87
87
|
with:
|
|
88
88
|
ai_provider: gemini
|
|
89
89
|
ai_model: gemini-3-flash-preview
|
|
@@ -104,6 +104,7 @@ jobs:
|
|
|
104
104
|
| `mcp_servers_config` | No | JSON array of MCP server configs |
|
|
105
105
|
| `slack_webhook_url` | No | Slack webhook URL for notifications |
|
|
106
106
|
| `post_pr_comment` | No | Post results as PR comment (default: `false`) |
|
|
107
|
+
| `submit_review` | No | Submit formal PR review with APPROVE/REQUEST_CHANGES (default: `false`) |
|
|
107
108
|
| `github_token` | No | GitHub token (default: `${{ github.token }}`) |
|
|
108
109
|
|
|
109
110
|
## Outputs
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
with:
|
|
38
38
|
fetch-depth: 0
|
|
39
39
|
|
|
40
|
-
- uses: redhat-community-ai-tools/cicaddy-action@v0.
|
|
40
|
+
- uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
|
|
41
41
|
with:
|
|
42
42
|
ai_provider: gemini
|
|
43
43
|
ai_model: gemini-3-flash-preview
|
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
with:
|
|
64
64
|
fetch-depth: 0
|
|
65
65
|
|
|
66
|
-
- uses: redhat-community-ai-tools/cicaddy-action@v0.
|
|
66
|
+
- uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
|
|
67
67
|
with:
|
|
68
68
|
ai_provider: gemini
|
|
69
69
|
ai_model: gemini-3-flash-preview
|
|
@@ -84,6 +84,7 @@ jobs:
|
|
|
84
84
|
| `mcp_servers_config` | No | JSON array of MCP server configs |
|
|
85
85
|
| `slack_webhook_url` | No | Slack webhook URL for notifications |
|
|
86
86
|
| `post_pr_comment` | No | Post results as PR comment (default: `false`) |
|
|
87
|
+
| `submit_review` | No | Submit formal PR review with APPROVE/REQUEST_CHANGES (default: `false`) |
|
|
87
88
|
| `github_token` | No | GitHub token (default: `${{ github.token }}`) |
|
|
88
89
|
|
|
89
90
|
## Outputs
|
|
@@ -34,6 +34,10 @@ inputs:
|
|
|
34
34
|
description: 'Post analysis results as PR comment (requires github-token with pull-requests: write)'
|
|
35
35
|
required: false
|
|
36
36
|
default: 'false'
|
|
37
|
+
submit_review:
|
|
38
|
+
description: 'Submit a formal PR review with APPROVE or REQUEST_CHANGES based on AI analysis (requires github-token with pull-requests: write)'
|
|
39
|
+
required: false
|
|
40
|
+
default: 'false'
|
|
37
41
|
github_token:
|
|
38
42
|
description: 'GitHub token for API access'
|
|
39
43
|
required: false
|
|
@@ -36,7 +36,15 @@ _to_abs() {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if [[ -n "${INPUT_TASK_FILE}" ]]; then
|
|
39
|
-
|
|
39
|
+
_ws_path="$(_to_abs "${INPUT_TASK_FILE}")"
|
|
40
|
+
if [[ -f "${_ws_path}" ]]; then
|
|
41
|
+
export AI_TASK_FILE="${_ws_path}"
|
|
42
|
+
elif [[ -f "/app/${INPUT_TASK_FILE}" ]]; then
|
|
43
|
+
# Fall back to bundled task files shipped with the action
|
|
44
|
+
export AI_TASK_FILE="/app/${INPUT_TASK_FILE}"
|
|
45
|
+
else
|
|
46
|
+
export AI_TASK_FILE="${_ws_path}"
|
|
47
|
+
fi
|
|
40
48
|
fi
|
|
41
49
|
export AI_TASK_PROMPT="${INPUT_TASK_PROMPT}"
|
|
42
50
|
if [[ -n "${INPUT_REPORT_TEMPLATE}" ]]; then
|
|
@@ -46,6 +54,7 @@ export MCP_SERVERS_CONFIG="${INPUT_MCP_SERVERS_CONFIG:-[]}"
|
|
|
46
54
|
export SLACK_WEBHOOK_URL="${INPUT_SLACK_WEBHOOK_URL}"
|
|
47
55
|
export GITHUB_TOKEN="${INPUT_GITHUB_TOKEN:-$GITHUB_TOKEN}"
|
|
48
56
|
export POST_PR_COMMENT="${INPUT_POST_PR_COMMENT:-false}"
|
|
57
|
+
export SUBMIT_REVIEW="${INPUT_SUBMIT_REVIEW:-false}"
|
|
49
58
|
|
|
50
59
|
# Extract PR number from GITHUB_REF (e.g. refs/pull/123/merge -> 123)
|
|
51
60
|
if [[ "${GITHUB_REF}" =~ ^refs/pull/([0-9]+)/ ]]; then
|
|
@@ -61,6 +61,11 @@ class Settings(CoreSettings):
|
|
|
61
61
|
validation_alias=AliasChoices("POST_PR_COMMENT"),
|
|
62
62
|
description="Whether to post analysis results as PR comment",
|
|
63
63
|
)
|
|
64
|
+
submit_review: bool = Field(
|
|
65
|
+
default=False,
|
|
66
|
+
validation_alias=AliasChoices("SUBMIT_REVIEW"),
|
|
67
|
+
description="Whether to submit a formal PR review (APPROVE or REQUEST_CHANGES)",
|
|
68
|
+
)
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
def load_settings() -> Settings:
|
|
@@ -95,6 +100,11 @@ def load_settings() -> Settings:
|
|
|
95
100
|
if post_pr:
|
|
96
101
|
env_data["post_pr_comment"] = post_pr.lower() in ("true", "1", "yes")
|
|
97
102
|
|
|
103
|
+
# Submit formal PR review flag
|
|
104
|
+
submit_review = os.getenv("SUBMIT_REVIEW", "").strip()
|
|
105
|
+
if submit_review:
|
|
106
|
+
env_data["submit_review"] = submit_review.lower() in ("true", "1", "yes")
|
|
107
|
+
|
|
98
108
|
# AI provider configuration
|
|
99
109
|
if os.getenv("AI_PROVIDER"):
|
|
100
110
|
env_data["ai_provider"] = os.getenv("AI_PROVIDER")
|
{cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/agents.py
RENAMED
|
@@ -18,6 +18,14 @@ logger = get_logger(__name__)
|
|
|
18
18
|
|
|
19
19
|
BOT_COMMENT_MARKER_PR_REVIEW = "<!-- cicaddy-action:pr-review -->"
|
|
20
20
|
|
|
21
|
+
# Pattern to detect a review verdict in the AI analysis output.
|
|
22
|
+
# The AI is instructed to include a line like "VERDICT: APPROVE" or
|
|
23
|
+
# "VERDICT: REQUEST_CHANGES" in its output.
|
|
24
|
+
_VERDICT_PATTERN = re.compile(
|
|
25
|
+
r"(?:^|\n)\s*(?:<!--\s*)?VERDICT:\s*(APPROVE|REQUEST_CHANGES)(?:\s*-->)?",
|
|
26
|
+
re.IGNORECASE,
|
|
27
|
+
)
|
|
28
|
+
|
|
21
29
|
# Pattern matches fenced code blocks (possibly indented by list nesting).
|
|
22
30
|
_FENCED_CODE_BLOCK = re.compile(
|
|
23
31
|
r"^([ \t]*(?:`{3,}|~{3,})[^\n]*)\n(.*?)\n([ \t]*(?:`{3,}|~{3,}))[ \t]*$",
|
|
@@ -50,6 +58,19 @@ _MARKDOWN_WRAPPER = re.compile(
|
|
|
50
58
|
)
|
|
51
59
|
|
|
52
60
|
|
|
61
|
+
def extract_review_verdict(ai_text: str) -> str:
|
|
62
|
+
"""Extract the review verdict from AI analysis output.
|
|
63
|
+
|
|
64
|
+
Looks for a ``VERDICT: APPROVE`` or ``VERDICT: REQUEST_CHANGES`` line
|
|
65
|
+
(optionally wrapped in an HTML comment). Defaults to ``COMMENT`` when
|
|
66
|
+
no explicit verdict is found.
|
|
67
|
+
"""
|
|
68
|
+
m = _VERDICT_PATTERN.search(ai_text)
|
|
69
|
+
if m:
|
|
70
|
+
return m.group(1).upper()
|
|
71
|
+
return "COMMENT"
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
def strip_markdown_wrapper(text: str) -> str:
|
|
54
75
|
"""Strip a wrapping ```markdown fence from the entire AI output.
|
|
55
76
|
|
|
@@ -223,11 +244,16 @@ class GitHubPRAgent(BaseAIAgent):
|
|
|
223
244
|
pr_context = self._prepare_dspy_context(context)
|
|
224
245
|
dspy_prompt = self.build_dspy_prompt(task_file, pr_context)
|
|
225
246
|
if dspy_prompt:
|
|
247
|
+
if getattr(self.settings, "submit_review", False):
|
|
248
|
+
dspy_prompt += self._verdict_instruction()
|
|
226
249
|
return dspy_prompt
|
|
227
250
|
|
|
228
251
|
pr_data = context["pull_request"]
|
|
229
252
|
diff_content = context["diff"]
|
|
230
253
|
|
|
254
|
+
submit_review = getattr(self.settings, "submit_review", False)
|
|
255
|
+
verdict_block = self._verdict_instruction() if submit_review else ""
|
|
256
|
+
|
|
231
257
|
return f"""You are an AI agent performing pull request code review.
|
|
232
258
|
|
|
233
259
|
Repository: {context.get("repository", "Unknown")}
|
|
@@ -249,7 +275,21 @@ Instructions:
|
|
|
249
275
|
4. Provide actionable, specific feedback
|
|
250
276
|
|
|
251
277
|
Please provide your comprehensive analysis in markdown format.
|
|
252
|
-
"""
|
|
278
|
+
{verdict_block}"""
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _verdict_instruction() -> str:
|
|
282
|
+
"""Return the prompt snippet that asks the AI to emit a verdict."""
|
|
283
|
+
return (
|
|
284
|
+
"\n\nIMPORTANT: At the very end of your analysis, you MUST include a verdict line "
|
|
285
|
+
"in an HTML comment with the format:\n"
|
|
286
|
+
"<!-- VERDICT: APPROVE -->\n"
|
|
287
|
+
"or\n"
|
|
288
|
+
"<!-- VERDICT: REQUEST_CHANGES -->\n\n"
|
|
289
|
+
"Use REQUEST_CHANGES when there are bugs, security issues, or significant problems "
|
|
290
|
+
"that must be fixed before merging. Use APPROVE when the changes look good overall "
|
|
291
|
+
"(minor suggestions are OK with APPROVE)."
|
|
292
|
+
)
|
|
253
293
|
|
|
254
294
|
def _prepare_dspy_context(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
255
295
|
"""Prepare context with PR-specific data for DSPy prompt building."""
|
|
@@ -265,7 +305,7 @@ Please provide your comprehensive analysis in markdown format.
|
|
|
265
305
|
return pr_context
|
|
266
306
|
|
|
267
307
|
async def send_notifications(self, report: dict[str, Any], analysis_result: dict[str, Any]):
|
|
268
|
-
"""Send notifications via PR comment and Slack."""
|
|
308
|
+
"""Send notifications via PR comment, formal review, and Slack."""
|
|
269
309
|
# Sanitize outputs
|
|
270
310
|
if "ai_analysis" in analysis_result:
|
|
271
311
|
analysis_result["ai_analysis"] = self.leak_detector.sanitize_text(
|
|
@@ -287,9 +327,47 @@ Please provide your comprehensive analysis in markdown format.
|
|
|
287
327
|
)
|
|
288
328
|
logger.debug("PR comment post traceback:", exc_info=True)
|
|
289
329
|
|
|
330
|
+
# Submit formal PR review if enabled
|
|
331
|
+
submit_review = getattr(self.settings, "submit_review", False)
|
|
332
|
+
if submit_review and self.platform_analyzer and self.pr_number:
|
|
333
|
+
try:
|
|
334
|
+
ai_text = analysis_result.get("ai_analysis", "")
|
|
335
|
+
verdict = extract_review_verdict(ai_text)
|
|
336
|
+
review_body = self._format_review_body(analysis_result)
|
|
337
|
+
await self.platform_analyzer.submit_pr_review(
|
|
338
|
+
int(self.pr_number), review_body, event=verdict
|
|
339
|
+
)
|
|
340
|
+
logger.info(f"Submitted {verdict} review on PR #{self.pr_number}")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(
|
|
343
|
+
f"Failed to submit PR review: {self.leak_detector.sanitize_text(str(e))}"
|
|
344
|
+
)
|
|
345
|
+
logger.debug("PR review submit traceback:", exc_info=True)
|
|
346
|
+
|
|
290
347
|
# Send Slack notification using parent class
|
|
291
348
|
await super().send_notifications(report, analysis_result)
|
|
292
349
|
|
|
350
|
+
def _format_review_body(self, analysis_result: dict[str, Any]) -> str:
|
|
351
|
+
"""Format analysis results as a PR review body.
|
|
352
|
+
|
|
353
|
+
Strips the VERDICT line from the output so it does not appear in
|
|
354
|
+
the rendered review.
|
|
355
|
+
"""
|
|
356
|
+
body = ""
|
|
357
|
+
if "ai_analysis" in analysis_result:
|
|
358
|
+
cleaned = strip_markdown_wrapper(analysis_result["ai_analysis"])
|
|
359
|
+
cleaned = dedent_code_blocks(cleaned)
|
|
360
|
+
# Remove the VERDICT line from the review body
|
|
361
|
+
cleaned = _VERDICT_PATTERN.sub("", cleaned).strip()
|
|
362
|
+
body = cleaned
|
|
363
|
+
|
|
364
|
+
body += (
|
|
365
|
+
"\n\n---\n"
|
|
366
|
+
"*Generated with [cicaddy-action]"
|
|
367
|
+
"(https://github.com/redhat-community-ai-tools/cicaddy-action)*"
|
|
368
|
+
)
|
|
369
|
+
return body
|
|
370
|
+
|
|
293
371
|
def _format_pr_comment(self, analysis_result: dict[str, Any]) -> str:
|
|
294
372
|
"""Format analysis results as a PR comment.
|
|
295
373
|
|
{cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/analyzer.py
RENAMED
|
@@ -276,6 +276,23 @@ class GitHubAnalyzer:
|
|
|
276
276
|
|
|
277
277
|
return result
|
|
278
278
|
|
|
279
|
+
async def submit_pr_review(self, pr_number: int, body: str, event: str = "COMMENT") -> None:
|
|
280
|
+
"""Submit a formal pull request review.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
pr_number: Pull request number.
|
|
284
|
+
body: Review body text.
|
|
285
|
+
event: Review event type — one of ``APPROVE``, ``REQUEST_CHANGES``,
|
|
286
|
+
or ``COMMENT``.
|
|
287
|
+
"""
|
|
288
|
+
valid_events = {"APPROVE", "REQUEST_CHANGES", "COMMENT"}
|
|
289
|
+
if event not in valid_events:
|
|
290
|
+
raise ValueError(f"Invalid review event '{event}', must be one of {valid_events}")
|
|
291
|
+
|
|
292
|
+
pr = self.repo.get_pull(pr_number)
|
|
293
|
+
pr.create_review(body=body, event=event)
|
|
294
|
+
logger.info(f"Submitted {event} review on PR #{pr_number}")
|
|
295
|
+
|
|
279
296
|
def close(self):
|
|
280
297
|
"""Close the GitHub API connection."""
|
|
281
298
|
self.github.close()
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""Tests for dedent_code_blocks
|
|
1
|
+
"""Tests for dedent_code_blocks, strip_markdown_wrapper, and extract_review_verdict."""
|
|
2
2
|
|
|
3
|
-
from cicaddy_github.github_integration.agents import
|
|
3
|
+
from cicaddy_github.github_integration.agents import (
|
|
4
|
+
dedent_code_blocks,
|
|
5
|
+
extract_review_verdict,
|
|
6
|
+
strip_markdown_wrapper,
|
|
7
|
+
)
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
class TestDedentCodeBlocks:
|
|
@@ -111,3 +115,37 @@ class TestStripMarkdownWrapper:
|
|
|
111
115
|
"""A ```python wrapper is NOT stripped."""
|
|
112
116
|
text = "```python\nprint('hi')\n```"
|
|
113
117
|
assert strip_markdown_wrapper(text) == text
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestExtractReviewVerdict:
|
|
121
|
+
"""Test extraction of review verdict from AI analysis output."""
|
|
122
|
+
|
|
123
|
+
def test_extracts_approve(self):
|
|
124
|
+
text = "Analysis looks good.\n<!-- VERDICT: APPROVE -->"
|
|
125
|
+
assert extract_review_verdict(text) == "APPROVE"
|
|
126
|
+
|
|
127
|
+
def test_extracts_request_changes(self):
|
|
128
|
+
text = "Found bugs.\n<!-- VERDICT: REQUEST_CHANGES -->"
|
|
129
|
+
assert extract_review_verdict(text) == "REQUEST_CHANGES"
|
|
130
|
+
|
|
131
|
+
def test_extracts_plain_verdict(self):
|
|
132
|
+
"""Verdict without HTML comment wrapper."""
|
|
133
|
+
text = "Analysis.\nVERDICT: APPROVE"
|
|
134
|
+
assert extract_review_verdict(text) == "APPROVE"
|
|
135
|
+
|
|
136
|
+
def test_case_insensitive(self):
|
|
137
|
+
text = "Analysis.\n<!-- verdict: request_changes -->"
|
|
138
|
+
assert extract_review_verdict(text) == "REQUEST_CHANGES"
|
|
139
|
+
|
|
140
|
+
def test_defaults_to_comment(self):
|
|
141
|
+
"""No verdict line returns COMMENT."""
|
|
142
|
+
text = "Just some analysis text without a verdict."
|
|
143
|
+
assert extract_review_verdict(text) == "COMMENT"
|
|
144
|
+
|
|
145
|
+
def test_verdict_with_leading_whitespace(self):
|
|
146
|
+
text = "Analysis.\n <!-- VERDICT: APPROVE -->"
|
|
147
|
+
assert extract_review_verdict(text) == "APPROVE"
|
|
148
|
+
|
|
149
|
+
def test_verdict_in_middle_of_text(self):
|
|
150
|
+
text = "Start.\n<!-- VERDICT: REQUEST_CHANGES -->\nEnd."
|
|
151
|
+
assert extract_review_verdict(text) == "REQUEST_CHANGES"
|
|
@@ -261,6 +261,49 @@ class TestPostPRComment:
|
|
|
261
261
|
mock_pr.create_issue_comment.assert_called_once()
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
class TestSubmitPRReview:
|
|
265
|
+
"""Test formal PR review submission."""
|
|
266
|
+
|
|
267
|
+
@pytest.mark.asyncio
|
|
268
|
+
async def test_submits_approve_review(self, analyzer, mock_github):
|
|
269
|
+
"""Submits an APPROVE review via PyGithub."""
|
|
270
|
+
_, mock_repo = mock_github
|
|
271
|
+
mock_pr = MagicMock()
|
|
272
|
+
mock_repo.get_pull.return_value = mock_pr
|
|
273
|
+
|
|
274
|
+
await analyzer.submit_pr_review(42, "LGTM!", event="APPROVE")
|
|
275
|
+
|
|
276
|
+
mock_pr.create_review.assert_called_once_with(body="LGTM!", event="APPROVE")
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_submits_request_changes_review(self, analyzer, mock_github):
|
|
280
|
+
"""Submits a REQUEST_CHANGES review."""
|
|
281
|
+
_, mock_repo = mock_github
|
|
282
|
+
mock_pr = MagicMock()
|
|
283
|
+
mock_repo.get_pull.return_value = mock_pr
|
|
284
|
+
|
|
285
|
+
await analyzer.submit_pr_review(42, "Please fix.", event="REQUEST_CHANGES")
|
|
286
|
+
|
|
287
|
+
mock_pr.create_review.assert_called_once_with(body="Please fix.", event="REQUEST_CHANGES")
|
|
288
|
+
|
|
289
|
+
@pytest.mark.asyncio
|
|
290
|
+
async def test_submits_comment_review_by_default(self, analyzer, mock_github):
|
|
291
|
+
"""Default event is COMMENT."""
|
|
292
|
+
_, mock_repo = mock_github
|
|
293
|
+
mock_pr = MagicMock()
|
|
294
|
+
mock_repo.get_pull.return_value = mock_pr
|
|
295
|
+
|
|
296
|
+
await analyzer.submit_pr_review(42, "Some feedback.")
|
|
297
|
+
|
|
298
|
+
mock_pr.create_review.assert_called_once_with(body="Some feedback.", event="COMMENT")
|
|
299
|
+
|
|
300
|
+
@pytest.mark.asyncio
|
|
301
|
+
async def test_rejects_invalid_event(self, analyzer):
|
|
302
|
+
"""Invalid review event raises ValueError."""
|
|
303
|
+
with pytest.raises(ValueError, match="Invalid review event"):
|
|
304
|
+
await analyzer.submit_pr_review(42, "body", event="INVALID")
|
|
305
|
+
|
|
306
|
+
|
|
264
307
|
class TestBuildUpdatedBody:
|
|
265
308
|
"""Test the history collapsing logic."""
|
|
266
309
|
|
|
@@ -174,3 +174,31 @@ class TestSettingsValidation:
|
|
|
174
174
|
|
|
175
175
|
settings = load_settings()
|
|
176
176
|
assert settings.post_pr_comment is True
|
|
177
|
+
|
|
178
|
+
def test_submit_review_default_false(self):
|
|
179
|
+
"""SUBMIT_REVIEW defaults to False."""
|
|
180
|
+
env = {
|
|
181
|
+
"AI_PROVIDER": "gemini",
|
|
182
|
+
"AI_MODEL": "gemini-2.5-flash",
|
|
183
|
+
"MCP_SERVERS_CONFIG": "[]",
|
|
184
|
+
}
|
|
185
|
+
with patch.dict(os.environ, env, clear=False):
|
|
186
|
+
os.environ.pop("SUBMIT_REVIEW", None)
|
|
187
|
+
from cicaddy_github.config.settings import load_settings
|
|
188
|
+
|
|
189
|
+
settings = load_settings()
|
|
190
|
+
assert settings.submit_review is False
|
|
191
|
+
|
|
192
|
+
def test_submit_review_true(self):
|
|
193
|
+
"""SUBMIT_REVIEW=true sets field to True."""
|
|
194
|
+
env = {
|
|
195
|
+
"SUBMIT_REVIEW": "true",
|
|
196
|
+
"AI_PROVIDER": "gemini",
|
|
197
|
+
"AI_MODEL": "gemini-2.5-flash",
|
|
198
|
+
"MCP_SERVERS_CONFIG": "[]",
|
|
199
|
+
}
|
|
200
|
+
with patch.dict(os.environ, env, clear=False):
|
|
201
|
+
from cicaddy_github.config.settings import load_settings
|
|
202
|
+
|
|
203
|
+
settings = load_settings()
|
|
204
|
+
assert settings.submit_review is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/__init__.py
RENAMED
|
File without changes
|
{cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/detector.py
RENAMED
|
File without changes
|
{cicaddy_github-0.3.1 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|