cicaddy-github 0.1.0__tar.gz → 0.3.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.
Files changed (43) hide show
  1. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/Dockerfile +1 -1
  2. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/PKG-INFO +1 -1
  3. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/pyproject.toml +1 -1
  4. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/github_integration/agents.py +48 -1
  5. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tasks/pr_review.yml +3 -0
  6. cicaddy_github-0.3.0/tests/unit/test_agents.py +113 -0
  7. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.claude/skills/cicaddy-action/SKILL.md +0 -0
  8. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.github/dependabot.yml +0 -0
  9. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.github/workflows/changelog.yml +0 -0
  10. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.github/workflows/ci.yml +0 -0
  11. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.github/workflows/pr-review.yml +0 -0
  12. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.github/workflows/release.yml +0 -0
  13. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.gitignore +0 -0
  14. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/.pre-commit-config.yaml +0 -0
  15. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/CLAUDE.md +0 -0
  16. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  17. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/CONTRIBUTING.md +0 -0
  18. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/LICENSE +0 -0
  19. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/README.md +0 -0
  20. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/action.yml +0 -0
  21. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/entrypoint.sh +0 -0
  22. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/__init__.py +0 -0
  23. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/config/__init__.py +0 -0
  24. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/config/settings.py +0 -0
  25. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/github_integration/__init__.py +0 -0
  26. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/github_integration/analyzer.py +0 -0
  27. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/github_integration/detector.py +0 -0
  28. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/github_integration/tools.py +0 -0
  29. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/plugin.py +0 -0
  30. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/security/__init__.py +0 -0
  31. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/security/leak_detector.py +0 -0
  32. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/src/cicaddy_github/validation.py +0 -0
  33. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tasks/changelog_report.yml +0 -0
  34. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/templates/report_template.html +0 -0
  35. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/__init__.py +0 -0
  36. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/conftest.py +0 -0
  37. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/__init__.py +0 -0
  38. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_analyzer.py +0 -0
  39. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_detector.py +0 -0
  40. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_leak_detector.py +0 -0
  41. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_plugin.py +0 -0
  42. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_settings.py +0 -0
  43. {cicaddy_github-0.1.0 → cicaddy_github-0.3.0}/tests/unit/test_tools.py +0 -0
@@ -1,4 +1,4 @@
1
- FROM python:3.12-slim
1
+ FROM python:3.14-slim
2
2
 
3
3
  WORKDIR /app
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cicaddy-github
3
- Version: 0.1.0
3
+ Version: 0.3.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cicaddy-github"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "GitHub Actions plugin for cicaddy AI agent framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,8 @@
1
1
  """GitHub AI Agents for PR review and task execution."""
2
2
 
3
3
  import os
4
+ import re
5
+ import textwrap
4
6
  from typing import Any
5
7
 
6
8
  from cicaddy.agent.base import BaseAIAgent
@@ -16,6 +18,50 @@ logger = get_logger(__name__)
16
18
 
17
19
  BOT_COMMENT_MARKER_PR_REVIEW = "<!-- cicaddy-action:pr-review -->"
18
20
 
21
+ # Pattern matches fenced code blocks (possibly indented by list nesting).
22
+ _FENCED_CODE_BLOCK = re.compile(
23
+ r"^([ \t]*(?:`{3,}|~{3,})[^\n]*)\n(.*?)\n([ \t]*(?:`{3,}|~{3,}))[ \t]*$",
24
+ re.MULTILINE | re.DOTALL,
25
+ )
26
+
27
+
28
+ def dedent_code_blocks(text: str) -> str:
29
+ """Remove common leading whitespace from fenced code block content.
30
+
31
+ AI models often indent code block content to match surrounding list
32
+ indentation. GitHub renders that whitespace literally, so we strip
33
+ the common prefix using ``textwrap.dedent`` while preserving relative
34
+ indentation within the block.
35
+ """
36
+
37
+ def _dedent(match: re.Match) -> str:
38
+ opener = match.group(1).lstrip()
39
+ content = match.group(2)
40
+ closer = match.group(3).lstrip()
41
+ return f"{opener}\n{textwrap.dedent(content)}\n{closer}"
42
+
43
+ return _FENCED_CODE_BLOCK.sub(_dedent, text)
44
+
45
+
46
+ # Matches output wrapped in a single ```markdown ... ``` fence.
47
+ _MARKDOWN_WRAPPER = re.compile(
48
+ r"^\s*```(?:markdown|md)\s*\n(.*?)\n\s*```\s*$",
49
+ re.DOTALL | re.IGNORECASE,
50
+ )
51
+
52
+
53
+ def strip_markdown_wrapper(text: str) -> str:
54
+ """Strip a wrapping ```markdown fence from the entire AI output.
55
+
56
+ Some models interpret ``output_format: markdown`` as "wrap the response
57
+ in a markdown code fence", which causes GitHub to render the comment as
58
+ a literal code block instead of formatted markdown.
59
+ """
60
+ m = _MARKDOWN_WRAPPER.match(text.strip())
61
+ if m:
62
+ return m.group(1)
63
+ return text
64
+
19
65
 
20
66
  class GitHubTaskAgent(BaseAIAgent):
21
67
  """AI Agent for scheduled tasks and changelog generation."""
@@ -254,7 +300,8 @@ Please provide your comprehensive analysis in markdown format.
254
300
  comment = f"{BOT_COMMENT_MARKER_PR_REVIEW}\n"
255
301
 
256
302
  if "ai_analysis" in analysis_result:
257
- comment += analysis_result["ai_analysis"] + "\n"
303
+ cleaned = strip_markdown_wrapper(analysis_result["ai_analysis"])
304
+ comment += dedent_code_blocks(cleaned) + "\n"
258
305
 
259
306
  comment += (
260
307
  "\n<!-- cicaddy-footer -->\n---\n"
@@ -69,6 +69,9 @@ constraints:
69
69
  - Assess test coverage implications
70
70
  - When reviewing code that uses external libraries, use Context7 tools to look up
71
71
  current API documentation and best practices
72
+ - All fenced code blocks (```) must start at column 0 with no leading whitespace,
73
+ even when inside list items. Content inside code blocks must not have extra
74
+ indentation from list nesting
72
75
 
73
76
  reasoning: chain_of_thought
74
77
  output_format: markdown
@@ -0,0 +1,113 @@
1
+ """Tests for dedent_code_blocks and strip_markdown_wrapper in agents module."""
2
+
3
+ from cicaddy_github.github_integration.agents import dedent_code_blocks, strip_markdown_wrapper
4
+
5
+
6
+ class TestDedentCodeBlocks:
7
+ """Test code block dedenting for AI-generated markdown."""
8
+
9
+ def test_dedents_indented_code_block(self):
10
+ """Code block content indented by list nesting is dedented."""
11
+ text = (
12
+ "* **Example:**\n"
13
+ " ```diff\n"
14
+ " --- a/file.txt\n"
15
+ " +++ b/file.txt\n"
16
+ " @@ -1,3 +1,4 @@\n"
17
+ " ```"
18
+ )
19
+ result = dedent_code_blocks(text)
20
+ assert "```diff\n--- a/file.txt\n+++ b/file.txt" in result
21
+
22
+ def test_preserves_relative_indentation(self):
23
+ """Relative indentation within the code block is preserved."""
24
+ text = " ```python\n def foo():\n return 42\n ```"
25
+ result = dedent_code_blocks(text)
26
+ assert "```python\ndef foo():\n return 42\n```" in result
27
+
28
+ def test_no_change_for_unindented_blocks(self):
29
+ """Already flush code blocks are unchanged."""
30
+ text = "```python\ndef foo():\n return 42\n```"
31
+ result = dedent_code_blocks(text)
32
+ assert result == text
33
+
34
+ def test_multiple_code_blocks(self):
35
+ """Multiple code blocks in the same text are all dedented."""
36
+ text = (
37
+ "Item 1:\n"
38
+ " ```python\n"
39
+ " print('a')\n"
40
+ " ```\n"
41
+ "\n"
42
+ "Item 2:\n"
43
+ " ```bash\n"
44
+ " echo hello\n"
45
+ " ```"
46
+ )
47
+ result = dedent_code_blocks(text)
48
+ assert "```python\nprint('a')\n```" in result
49
+ assert "```bash\necho hello\n```" in result
50
+
51
+ def test_tilde_delimiters(self):
52
+ """Tilde-fenced code blocks are also dedented."""
53
+ text = " ~~~python\n print('hello')\n ~~~"
54
+ result = dedent_code_blocks(text)
55
+ assert "~~~python\nprint('hello')\n~~~" in result
56
+
57
+ def test_trailing_whitespace_on_closer(self):
58
+ """Trailing whitespace after closing fence is tolerated."""
59
+ text = " ```python\n x = 1\n ``` "
60
+ result = dedent_code_blocks(text)
61
+ assert "```python\nx = 1\n```" in result
62
+
63
+ def test_text_without_code_blocks(self):
64
+ """Plain text without code blocks is unchanged."""
65
+ text = "This is plain markdown with **bold** and *italic*."
66
+ assert dedent_code_blocks(text) == text
67
+
68
+ def test_mixed_content_preserves_surrounding_text(self):
69
+ """List structure around code blocks is preserved."""
70
+ text = "* Item one\n ```diff\n +added line\n ```\n* Item two"
71
+ result = dedent_code_blocks(text)
72
+ assert result.startswith("* Item one\n")
73
+ assert result.endswith("* Item two")
74
+ assert "+added line" in result
75
+
76
+
77
+ class TestStripMarkdownWrapper:
78
+ """Test stripping wrapping ```markdown fences from AI output."""
79
+
80
+ def test_strips_markdown_wrapper(self):
81
+ """Output wrapped in ```markdown is unwrapped."""
82
+ text = "```markdown\n### Summary\nSome analysis.\n```"
83
+ result = strip_markdown_wrapper(text)
84
+ assert result == "### Summary\nSome analysis."
85
+
86
+ def test_strips_md_wrapper(self):
87
+ """Output wrapped in ```md is also unwrapped."""
88
+ text = "```md\n## Title\nContent.\n```"
89
+ result = strip_markdown_wrapper(text)
90
+ assert result == "## Title\nContent."
91
+
92
+ def test_strips_case_insensitive(self):
93
+ """Output wrapped in ```Markdown or ```MD is also unwrapped."""
94
+ for tag in ("Markdown", "MARKDOWN", "MD", "Md"):
95
+ text = f"```{tag}\nContent here.\n```"
96
+ result = strip_markdown_wrapper(text)
97
+ assert result == "Content here.", f"Failed for tag: {tag}"
98
+
99
+ def test_no_change_without_wrapper(self):
100
+ """Plain markdown without wrapper is unchanged."""
101
+ text = "### Summary\nSome analysis."
102
+ assert strip_markdown_wrapper(text) == text
103
+
104
+ def test_preserves_internal_code_blocks(self):
105
+ """Code blocks inside the markdown wrapper are preserved."""
106
+ text = "```markdown\n### Example\n```python\nprint('hi')\n```\n```"
107
+ result = strip_markdown_wrapper(text)
108
+ assert "```python\nprint('hi')\n```" in result
109
+
110
+ def test_no_change_for_non_markdown_fence(self):
111
+ """A ```python wrapper is NOT stripped."""
112
+ text = "```python\nprint('hi')\n```"
113
+ assert strip_markdown_wrapper(text) == text
File without changes
File without changes
File without changes