Skip to content

Commit b4ed3a9

Browse files
authored
Merge pull request packit#81 from TomasTomecek/backporting-ensure-prep-git-repo
Backporting process improvements:
2 parents 8388448 + 53a096f commit b4ed3a9

File tree

5 files changed

+106
-15
lines changed

5 files changed

+106
-15
lines changed

beeai/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ run-backport-agent-standalone:
4444
-e JIRA_ISSUE=$(JIRA_ISSUE) \
4545
-e BRANCH=$(BRANCH) \
4646
-e DRY_RUN=$(DRY_RUN) \
47+
-e CVE_ID=$(CVE_ID) \
4748
backport-agent
4849

4950

beeai/agents/backport_agent.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from tools.specfile import AddChangelogEntryTool, BumpReleaseTool
2727
from tools.text import CreateTool, InsertTool, StrReplaceTool, ViewTool
28-
from tools.wicked_git import GitPatchCreationTool
28+
from tools.wicked_git import GitLogSearchTool, GitPatchCreationTool
2929
from constants import COMMIT_PREFIX, BRANCH_PREFIX
3030
from observability import setup_observability
3131
from tools.commands import RunShellCommandTool
@@ -39,15 +39,8 @@ class InputSchema(BaseModel):
3939
package: str = Field(description="Package to update")
4040
upstream_fix: str = Field(description="Link to an upstream fix for the issue")
4141
jira_issue: str = Field(description="Jira issue to reference as resolved")
42+
cve_id: str = Field(default="", description="CVE ID if the jira issue is a CVE")
4243
dist_git_branch: str = Field(description="Git branch in dist-git to be updated")
43-
gitlab_user: str = Field(
44-
description="Name of the GitLab user",
45-
default=os.getenv("GITLAB_USER", "rhel-packaging-agent"),
46-
)
47-
git_url: str = Field(
48-
description="URL of the git repository",
49-
default="https://gitlab.com/redhat/centos-stream/rpms",
50-
)
5144
git_repo_basepath: str = Field(
5245
description="Base path for cloned git repos",
5346
default=os.getenv("GIT_REPO_BASEPATH"),
@@ -68,8 +61,12 @@ class OutputSchema(BaseModel):
6861
def render_prompt(input: InputSchema) -> str:
6962
template = (
7063
'Work inside the repository cloned at "{{ git_repo_basepath }}/{{ package }}"\n'
64+
"Use the `git_log_search` tool to check if the jira issue ({{ jira_issue }}) or CVE ({{ cve_id }}) is already resolved.\n"
65+
"If the issue or the cve are already resolved, exit the backporting process with success=True and status=\"Backport already applied\"\n"
7166
"Download the upstream fix from {{ upstream_fix }}\n"
7267
'Store the patch file as "{{ jira_issue }}.patch" in the repository root\n'
68+
"If directory {{ unpacked_sources }} is not a git repository, run `git init` in it "
69+
"and create an initial commit\n"
7370
"Navigate to the directory {{ unpacked_sources }} and use `git am --reject` "
7471
"command to apply the patch {{ jira_issue }}.patch\n"
7572
"Resolve all conflicts inside {{ unpacked_sources }} directory and "
@@ -89,7 +86,6 @@ def backport_git_steps(data):
8986
commit_title=f"{COMMIT_PREFIX} backport {input_data.jira_issue}",
9087
files_to_commit=f"*.spec and {input_data.jira_issue}.patch",
9188
branch_name=f"{BRANCH_PREFIX}-{input_data.jira_issue}",
92-
git_url=input_data.git_url,
9389
dist_git_branch=input_data.dist_git_branch,
9490
)
9591

@@ -142,6 +138,7 @@ async def main() -> None:
142138
logging.basicConfig(level=logging.INFO)
143139

144140
setup_observability(os.getenv("COLLECTOR_ENDPOINT"))
141+
cve_id = os.getenv("CVE_ID", "")
145142

146143
async with mcp_tools(os.getenv("MCP_GATEWAY_URL")) as gateway_tools:
147144
agent = RequirementAgent(
@@ -155,6 +152,7 @@ async def main() -> None:
155152
InsertTool(),
156153
StrReplaceTool(),
157154
GitPatchCreationTool(),
155+
GitLogSearchTool(),
158156
BumpReleaseTool(),
159157
AddChangelogEntryTool(),
160158
]
@@ -206,6 +204,7 @@ async def run(input):
206204
upstream_fix=upstream_fix,
207205
jira_issue=jira_issue,
208206
dist_git_branch=branch,
207+
cve_id=cve_id,
209208
)
210209
unpacked_sources, local_clone = prepare_package(package, jira_issue, branch, input)
211210
input.unpacked_sources = str(unpacked_sources)
@@ -251,10 +250,12 @@ class Task(BaseModel):
251250
upstream_fix=backport_data.patch_url,
252251
jira_issue=backport_data.jira_issue,
253252
dist_git_branch=backport_data.branch,
253+
cve_id=backport_data.cve_id,
254254
)
255-
input.unpacked_sources, local_clone = prepare_package(
255+
unpacked_sources, local_clone = prepare_package(
256256
backport_data.package, backport_data.jira_issue, backport_data.branch, input
257257
)
258+
input.unpacked_sources = str(unpacked_sources)
258259

259260
async def retry(task, error):
260261
task.attempts += 1

beeai/agents/tests/unit/test_tools.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88

99
from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware
1010

11-
from tools.wicked_git import GitPatchCreationTool, GitPatchCreationToolInput
11+
from tools.wicked_git import (
12+
GitPatchCreationTool,
13+
GitPatchCreationToolInput,
14+
GitLogSearchTool,
15+
GitLogSearchToolInput,
16+
)
1217
from tools.commands import RunShellCommandTool, RunShellCommandToolInput
1318
from tools.specfile import (
1419
AddChangelogEntryTool,
@@ -306,10 +311,12 @@ def git_repo(tmp_path):
306311
file_path = repo_path / "file.txt"
307312
file_path.write_text("Line 1\n")
308313
subprocess.run(["git", "add", "file.txt"], cwd=repo_path, check=True)
309-
subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=repo_path, check=True)
314+
subprocess.run(["git", "commit", "-m", "Initial commit\n\nCVE-2025-12345"],
315+
cwd=repo_path, check=True)
310316
file_path.write_text("Line1\nLine 2\n")
311317
subprocess.run(["git", "add", "file.txt"], cwd=repo_path, check=True)
312-
subprocess.run(["git", "commit", "-m", "Initial commit2"], cwd=repo_path, check=True)
318+
subprocess.run(["git", "commit", "-m", "Initial commit2\n\nResolves: RHEL-123456"],
319+
cwd=repo_path, check=True)
313320
subprocess.run(["git", "branch", "line-2"], cwd=repo_path, check=True)
314321
return repo_path
315322

@@ -348,3 +355,26 @@ async def test_git_patch_creation_tool_success(git_repo, tmp_path):
348355
assert output_patch.exists()
349356
# The patch file should contain the commit message "Add line 3"
350357
assert "Add line 3" in output_patch.read_text()
358+
359+
360+
@pytest.mark.asyncio
361+
@pytest.mark.parametrize(
362+
"cve_id, jira_issue, expected",
363+
[
364+
("CVE-2025-12345", "", "Found 1 matching commit(s) for 'CVE-2025-12345'"),
365+
("CVE-2025-12346", "", "No matches found for 'CVE-2025-12346'"),
366+
("", "RHEL-123456", "Found 1 matching commit(s) for 'RHEL-123456'"),
367+
("", "RHEL-123457", "No matches found for 'RHEL-123457'"),
368+
]
369+
)
370+
async def test_git_log_search_tool_found(git_repo, cve_id, jira_issue, expected):
371+
tool = GitLogSearchTool()
372+
output = await tool.run(
373+
input=GitLogSearchToolInput(
374+
repository_path=str(git_repo),
375+
cve_id=cve_id,
376+
jira_issue=jira_issue,
377+
)
378+
).middleware(GlobalTrajectoryMiddleware(pretty=True))
379+
result = output.result
380+
assert expected in result

beeai/agents/tools/wicked_git.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,61 @@ async def _run(
9191
return StringToolOutput(result=f"Successfully created a patch file: {tool_input.patch_file_path}")
9292
except Exception as e:
9393
# we absolutely need to do this otherwise the error won't appear anywhere
94-
return StringToolOutput(result=f"ERROR: {e}")
94+
return StringToolOutput(result=f"ERROR: {e}")
95+
96+
97+
class GitLogSearchToolInput(BaseModel):
98+
repository_path: str = Field(description="Absolute path to the git repository")
99+
cve_id: str = Field(description="CVE ID to look for in git history")
100+
jira_issue: str = Field(description="Jira issue to look for in git history")
101+
102+
103+
class GitLogSearchTool(Tool[GitLogSearchToolInput, ToolRunOptions, StringToolOutput]):
104+
name = "git_log_search"
105+
description = """
106+
Searches the git history for a reference to either the provided cve_id or jira_issue.
107+
Returns the commit hash and the commit message.
108+
"""
109+
input_schema = GitLogSearchToolInput
110+
111+
def _create_emitter(self) -> Emitter:
112+
return Emitter.root().child(
113+
namespace=["tool", "git", self.name],
114+
creator=self,
115+
)
116+
117+
async def _run(
118+
self, tool_input: GitLogSearchToolInput, options: ToolRunOptions | None, context: RunContext
119+
) -> StringToolOutput:
120+
repo_path = Path(tool_input.repository_path)
121+
if not repo_path.exists():
122+
return StringToolOutput(result=f"ERROR: Repository path does not exist: {repo_path}")
123+
124+
if not (repo_path / ".git").exists():
125+
return StringToolOutput(result=f"ERROR: Not a git repository: {repo_path}")
126+
search = tool_input.cve_id or tool_input.jira_issue
127+
if not search:
128+
return StringToolOutput(
129+
result="ERROR: No search string provided, jira_issue or cve_id is required")
130+
131+
cmd = [
132+
"git",
133+
"log",
134+
"--no-merges",
135+
f"--grep={search}",
136+
"-n", "1",
137+
f"--pretty=%s %H",
138+
]
139+
140+
result = await run_command(cmd, cwd=repo_path)
141+
if result["exit_code"] != 0:
142+
return StringToolOutput(result=f"ERROR: Git command failed: {result['stderr']}")
143+
144+
output = (result["stdout"] or "").strip()
145+
if not output:
146+
return StringToolOutput(result=f"No matches found for '{search}'")
147+
148+
lines = output.splitlines()
149+
header = f"Found {len(lines)} matching commit(s) for '{search}'"
150+
# We do not return the output because it could confuse the agent
151+
return StringToolOutput(result=header)

beeai/agents/triage_agent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class BackportData(BaseModel):
5353
patch_url: str = Field(description="URL or reference to the source of the fix")
5454
justification: str = Field(description="Clear explanation of why this patch fixes the issue")
5555
jira_issue: str = Field(description="Jira issue identifier")
56+
cve_id: str = Field(description="CVE identifier")
5657

5758

5859
class ClarificationNeededData(BaseModel):
@@ -212,6 +213,7 @@ def render_prompt(input: InputSchema) -> str:
212213
PACKAGE: [package name]
213214
BRANCH: [target branch]
214215
PATCH_URL: [URL or reference to the source of the fix]
216+
CVE_ID: [CVE identifier, leave blank if not applicable]
215217
JUSTIFICATION: [A brief but clear explanation of why this patch fixes the issue, linking it to the root cause.]
216218
217219
If Clarification Needed:

0 commit comments

Comments
 (0)