1
1
import asyncio
2
2
import logging
3
3
import os
4
+ from shutil import rmtree
5
+ from pathlib import Path
6
+ import subprocess
4
7
import sys
5
8
import traceback
6
9
from typing import Optional
20
23
from beeai_framework .tools .think import ThinkTool
21
24
22
25
from 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
23
29
from constants import COMMIT_PREFIX , BRANCH_PREFIX
24
30
from observability import setup_observability
25
31
from tools .commands import RunShellCommandTool
@@ -50,6 +56,10 @@ class InputSchema(BaseModel):
50
56
description = "Base path for cloned git repos" ,
51
57
default = os .getenv ("GIT_REPO_BASEPATH" ),
52
58
)
59
+ unpacked_sources : str = Field (
60
+ description = "Path to the unpacked (using `centpkg prep`) sources" ,
61
+ default = "" ,
62
+ )
53
63
54
64
55
65
class OutputSchema (BaseModel ):
@@ -63,12 +73,37 @@ class BackportAgent(BaseAgent):
63
73
def __init__ (self ) -> None :
64
74
super ().__init__ (
65
75
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
+ ],
67
88
memory = UnconstrainedMemory (),
68
89
requirements = [
69
90
ConditionalRequirement (ThinkTool , force_after = Tool , consecutive_allowed = False ),
70
91
],
71
92
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 <[email protected] >\" " ,
105
+ "* IMPORTANT: Only perform changes relevant to the backport update"
106
+ ]
72
107
)
73
108
74
109
@property
@@ -89,8 +124,6 @@ def backport_git_steps(data: dict) -> str:
89
124
commit_title = f"{ COMMIT_PREFIX } backport { input_data .jira_issue } " ,
90
125
files_to_commit = f"*.spec and { input_data .jira_issue } .patch" ,
91
126
branch_name = f"{ BRANCH_PREFIX } -{ input_data .jira_issue } " ,
92
- git_user = input_data .git_user ,
93
- git_email = input_data .git_email ,
94
127
git_url = input_data .git_url ,
95
128
dist_git_branch = input_data .dist_git_branch ,
96
129
)
@@ -108,41 +141,19 @@ def backport_git_steps(data: dict) -> str:
108
141
109
142
@property
110
143
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
+ )
146
157
147
158
async def run_with_schema (self , input : TInputSchema ) -> TOutputSchema :
148
159
async with mcp_tools (
@@ -162,11 +173,52 @@ async def run_with_schema(self, input: TInputSchema) -> TOutputSchema:
162
173
requirement ._source_tool = None
163
174
164
175
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
+
165
216
async def main () -> None :
166
217
logging .basicConfig (level = logging .INFO )
167
218
168
219
setup_observability (os .getenv ("COLLECTOR_ENDPOINT" ))
169
220
agent = BackportAgent ()
221
+ dry_run = os .getenv ("DRY_RUN" , "False" ).lower () == "true"
170
222
171
223
if (
172
224
(package := os .getenv ("PACKAGE" , None ))
@@ -181,7 +233,16 @@ async def main() -> None:
181
233
jira_issue = jira_issue ,
182
234
dist_git_branch = branch ,
183
235
)
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 } " )
185
246
logger .info (f"Direct run completed: { output .model_dump_json (indent = 4 )} " )
186
247
return
187
248
@@ -215,6 +276,8 @@ class Task(BaseModel):
215
276
jira_issue = backport_data .jira_issue ,
216
277
dist_git_branch = backport_data .branch ,
217
278
)
279
+ input .unpacked_sources , local_clone = prepare_package (backport_data .package ,
280
+ backport_data .jira_issue , backport_data .branch , input )
218
281
219
282
async def retry (task , error ):
220
283
task .attempts += 1
@@ -238,7 +301,9 @@ async def retry(task, error):
238
301
await retry (
239
302
task , ErrorData (details = error , jira_issue = input .jira_issue ).model_dump_json ()
240
303
)
304
+ rmtree (local_clone )
241
305
else :
306
+ rmtree (local_clone )
242
307
if output .success :
243
308
logger .info (f"Backport successful for { backport_data .jira_issue } , "
244
309
f"adding to completed list" )
0 commit comments