11import asyncio
22import logging
33import os
4+ from shutil import rmtree
5+ from pathlib import Path
6+ import subprocess
47import sys
58import traceback
69from typing import Optional
2023from beeai_framework .tools .think import ThinkTool
2124
2225from base_agent import BaseAgent , TInputSchema , TOutputSchema
26+ from tools .specfile import AddChangelogEntryTool , BumpReleaseTool
27+ from tools .text import CreateTool , InsertTool , StrReplaceTool , ViewTool
28+ from tools .wicked_git import GitPatchCreationTool
2329from constants import COMMIT_PREFIX , BRANCH_PREFIX
2430from observability import setup_observability
2531from tools .commands import RunShellCommandTool
@@ -50,6 +56,10 @@ class InputSchema(BaseModel):
5056 description = "Base path for cloned git repos" ,
5157 default = os .getenv ("GIT_REPO_BASEPATH" ),
5258 )
59+ unpacked_sources : str = Field (
60+ description = "Path to the unpacked (using `centpkg prep`) sources" ,
61+ default = "" ,
62+ )
5363
5464
5565class OutputSchema (BaseModel ):
@@ -63,12 +73,37 @@ class BackportAgent(BaseAgent):
6373 def __init__ (self ) -> None :
6474 super ().__init__ (
6575 llm = ChatModel .from_name (os .getenv ("CHAT_MODEL" )),
66- tools = [ThinkTool (), RunShellCommandTool (), DuckDuckGoSearchTool ()],
76+ tools = [
77+ ThinkTool (),
78+ RunShellCommandTool (),
79+ DuckDuckGoSearchTool (),
80+ CreateTool (),
81+ ViewTool (),
82+ InsertTool (),
83+ StrReplaceTool (),
84+ GitPatchCreationTool (),
85+ BumpReleaseTool (),
86+ AddChangelogEntryTool (),
87+ ],
6788 memory = UnconstrainedMemory (),
6889 requirements = [
6990 ConditionalRequirement (ThinkTool , force_after = Tool , consecutive_allowed = False ),
7091 ],
7192 middlewares = [GlobalTrajectoryMiddleware (pretty = True )],
93+ role = "Red Hat Enterprise Linux developer" ,
94+ instructions = [
95+ "Use the `think` tool to reason through complex decisions and document your approach." ,
96+ "Preserve existing formatting and style conventions in RPM spec files and patch headers." ,
97+ "Use `rpmlint *.spec` to check for packaging issues and address any NEW errors" ,
98+ "Ignore pre-existing rpmlint warnings unless they're related to your changes" ,
99+ "Run `centpkg prep` to verify all patches apply cleanly during build preparation" ,
100+ "Generate an SRPM using `centpkg srpm` command to ensure complete build readiness" ,
101+ "Increment the 'Release' field in the .spec file following RPM packaging conventions "
102+ "using the `bump_release` tool" ,
103+ "Add a new changelog entry to the .spec file using the `add_changelog_entry` tool using name "
104+ "\" RHEL Packaging Agent <jotnar@redhat.com>\" " ,
105+ "* IMPORTANT: Only perform changes relevant to the backport update"
106+ ]
72107 )
73108
74109 @property
@@ -89,8 +124,6 @@ def backport_git_steps(data: dict) -> str:
89124 commit_title = f"{ COMMIT_PREFIX } backport { input_data .jira_issue } " ,
90125 files_to_commit = f"*.spec and { input_data .jira_issue } .patch" ,
91126 branch_name = f"{ BRANCH_PREFIX } -{ input_data .jira_issue } " ,
92- git_user = input_data .git_user ,
93- git_email = input_data .git_email ,
94127 git_url = input_data .git_url ,
95128 dist_git_branch = input_data .dist_git_branch ,
96129 )
@@ -108,41 +141,19 @@ def backport_git_steps(data: dict) -> str:
108141
109142 @property
110143 def prompt (self ) -> str :
111- return """
112- You are an agent for backporting a fix for a CentOS Stream package. You will prepare the content
113- of the update and then create a commit with the changes. Create a temporary directory and always work
114- inside it. Follow exactly these steps:
115-
116- 1. Find the location of the {{ package }} package at {{ git_url }}. Always use the {{ dist_git_branch }} branch.
117-
118- 2. Check if the package {{ package }} already has the fix {{ jira_issue }} applied.
119-
120- 3. Create a local Git repository by following these steps:
121- * Create a fork of the {{ package }} package using the `fork_repository` tool.
122- * Clone the fork using git and HTTPS into a temporary directory under {{ git_repo_basepath }}.
123- * Run command `centpkg sources` in the cloned repository which downloads all sources defined in the RPM specfile.
124- * Create a new Git branch named `automated-package-update-{{ jira_issue }}`.
125-
126- 4. Update the {{ package }} with the fix:
127- * Updating the 'Release' field in the .spec file as needed (or corresponding macros), following packaging
128- documentation.
129- * Make sure the format of the .spec file remains the same.
130- * Fetch the upstream fix {{ upstream_fix }} locally and store it in the git repo as "{{ jira_issue }}.patch".
131- * Add a new "Patch:" entry in the spec file for patch "{{ jira_issue }}.patch".
132- * Verify that the patch is being applied in the "%prep" section.
133- * Creating a changelog entry, referencing the Jira issue as "Resolves: <jira_issue>" for the issue {{ jira_issue }}.
134- The changelog entry has to use the current date.
135- * IMPORTANT: Only performing changes relevant to the backport update: Do not rename variables,
136- comment out existing lines, or alter if-else branches in the .spec file.
137-
138- 5. Verify and adjust the changes:
139- * Use `rpmlint` to validate your .spec file changes and fix any new errors it identifies.
140- * Generate the SRPM using `rpmbuild -bs` (ensure your .spec file and source files are correctly copied
141- to the build environment as required by the command).
142- * Verify the newly added patch applies cleanly using the command `centpkg prep`.
143-
144- 6. {{ backport_git_steps }}
145- """
144+ return (
145+ "Work inside the repository cloned at \" {{ git_repo_basepath }}/{{ package }}\" \n "
146+ "Download the upstream fix from {{ upstream_fix }}\n "
147+ "Store the patch file as \" {{ jira_issue }}.patch\" in the repository root\n "
148+ "Navigate to the directory {{ unpacked_sources }} and use `git am --reject` "
149+ "command to apply the patch {{ jira_issue }}.patch\n "
150+ "Resolve all conflicts inside {{ unpacked_sources }} directory and "
151+ "leave the repository in a dirty state\n "
152+ "Delete all *.rej files\n "
153+ "DO **NOT** RUN COMMAND `git am --continue`\n "
154+ "Once you resolve all conflicts, use tool git_patch_create to create a patch file\n "
155+ "{{ backport_git_steps }}"
156+ )
146157
147158 async def run_with_schema (self , input : TInputSchema ) -> TOutputSchema :
148159 async with mcp_tools (
@@ -162,11 +173,52 @@ async def run_with_schema(self, input: TInputSchema) -> TOutputSchema:
162173 requirement ._source_tool = None
163174
164175
176+ def prepare_package (package : str , jira_issue : str , dist_git_branch : str ,
177+ input_schema : InputSchema ) -> tuple [Path , Path ]:
178+ """
179+ Prepare the package for backporting by cloning the dist-git repository, switching to the appropriate branch,
180+ and downloading the sources.
181+ Returns the path to the unpacked sources.
182+ """
183+ git_repo = Path (input_schema .git_repo_basepath )
184+ git_repo .mkdir (parents = True , exist_ok = True )
185+ subprocess .check_call (
186+ [
187+ "centpkg" ,
188+ "clone" ,
189+ "--anonymous" ,
190+ "--branch" ,
191+ dist_git_branch ,
192+ package ,
193+ ],
194+ cwd = git_repo ,
195+ )
196+ local_clone = git_repo / package
197+ subprocess .check_call (
198+ [
199+ "git" ,
200+ "switch" ,
201+ "-c" ,
202+ f"automated-package-update-{ jira_issue } " ,
203+ dist_git_branch ,
204+ ],
205+ cwd = local_clone ,
206+ )
207+ subprocess .check_call (["centpkg" , "sources" ], cwd = local_clone )
208+ subprocess .check_call (["centpkg" , "prep" ], cwd = local_clone )
209+ unpacked_sources = list (local_clone .glob (f"*-build/*{ package } *" ))
210+ if len (unpacked_sources ) != 1 :
211+ raise ValueError (
212+ f"Expected exactly one unpacked source, got { unpacked_sources } "
213+ )
214+ return unpacked_sources [0 ], local_clone
215+
165216async def main () -> None :
166217 logging .basicConfig (level = logging .INFO )
167218
168219 setup_observability (os .getenv ("COLLECTOR_ENDPOINT" ))
169220 agent = BackportAgent ()
221+ dry_run = os .getenv ("DRY_RUN" , "False" ).lower () == "true"
170222
171223 if (
172224 (package := os .getenv ("PACKAGE" , None ))
@@ -181,7 +233,16 @@ async def main() -> None:
181233 jira_issue = jira_issue ,
182234 dist_git_branch = branch ,
183235 )
184- output = await agent .run_with_schema (input )
236+ unpacked_sources , local_clone = prepare_package (package , jira_issue , branch , input )
237+ input .unpacked_sources = str (unpacked_sources )
238+ try :
239+ output = await agent .run_with_schema (input )
240+ finally :
241+ if not dry_run :
242+ logger .info (f"Removing { local_clone } " )
243+ rmtree (local_clone )
244+ else :
245+ logger .info (f"DRY RUN: Not removing { local_clone } " )
185246 logger .info (f"Direct run completed: { output .model_dump_json (indent = 4 )} " )
186247 return
187248
@@ -215,6 +276,8 @@ class Task(BaseModel):
215276 jira_issue = backport_data .jira_issue ,
216277 dist_git_branch = backport_data .branch ,
217278 )
279+ input .unpacked_sources , local_clone = prepare_package (backport_data .package ,
280+ backport_data .jira_issue , backport_data .branch , input )
218281
219282 async def retry (task , error ):
220283 task .attempts += 1
@@ -238,7 +301,9 @@ async def retry(task, error):
238301 await retry (
239302 task , ErrorData (details = error , jira_issue = input .jira_issue ).model_dump_json ()
240303 )
304+ rmtree (local_clone )
241305 else :
306+ rmtree (local_clone )
242307 if output .success :
243308 logger .info (f"Backport successful for { backport_data .jira_issue } , "
244309 f"adding to completed list" )
0 commit comments