Skip to content

Commit 6f3550d

Browse files
committed
Run a reproducer in a simple way
1 parent 9906236 commit 6f3550d

9 files changed

Lines changed: 165 additions & 8 deletions

File tree

Containerfile.mcp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
FROM fedora:42
22

3+
RUN dnf -y copr enable @testing-farm/stable
4+
35
# Install system dependencies
46
RUN dnf -y install \
57
python3 \
@@ -16,6 +18,7 @@ RUN dnf -y install \
1618
krb5-workstation \
1719
centpkg \
1820
git \
21+
testing-farm \
1922
&& dnf clean all
2023

2124
# Install FastMCP

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ run-backport-agent-c9s-standalone:
6969
-e DRY_RUN=$(DRY_RUN) \
7070
-e MOCK_JIRA=$(MOCK_JIRA) \
7171
-e CVE_ID=$(CVE_ID) \
72+
-e REPRODUCER_INFO_REPO_URL=$(REPRODUCER_INFO_REPO_URL) \
73+
-e REPRODUCER_INFO_REPO_REF=$(REPRODUCER_INFO_REPO_REF) \
74+
-e REPRODUCER_INFO_TEST=$(REPRODUCER_INFO_TEST) \
7275
backport-agent-c9s
7376

7477
.PHONY: run-backport-agent-c10s-standalone
@@ -81,6 +84,9 @@ run-backport-agent-c10s-standalone:
8184
-e DRY_RUN=$(DRY_RUN) \
8285
-e MOCK_JIRA=$(MOCK_JIRA) \
8386
-e CVE_ID=$(CVE_ID) \
87+
-e REPRODUCER_INFO_REPO_URL=$(REPRODUCER_INFO_REPO_URL) \
88+
-e REPRODUCER_INFO_REPO_REF=$(REPRODUCER_INFO_REPO_REF) \
89+
-e REPRODUCER_INFO_TEST=$(REPRODUCER_INFO_TEST) \
8490
backport-agent-c10s
8591

8692
.PHONY: run-backport-agent-standalone

agents/backport_agent.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Task,
4040
)
4141
from common.utils import redis_client, fix_await
42+
from common.models import ReproducerInfo
4243
from constants import I_AM_JOTNAR, CAREFULLY_REVIEW_CHANGES
4344
from observability import setup_observability
4445
from tools.commands import RunShellCommandTool
@@ -288,6 +289,16 @@ def get_prompt() -> str:
288289
Everything from the previous attempt has been reset. Start over, follow the instructions from the start
289290
and don't forget to fix the issue.
290291
{{/build_error}}
292+
{{^reproducer_success}}
293+
The reproducer test case failed with the following error:
294+
295+
{{reproducer_result}}
296+
297+
Keep in mind the error and try to fix the patches in a way that makes the reproducer test case pass.
298+
299+
You can inspect the reproducer test case at {{reproducer_info.git_url}}/blob/{{reproducer_info.git_ref}}/{{reproducer_info.test}}.
300+
The test is located in the {{tmt_reproducer.git_ref}} branch of the {{tmt_reproducer.git_url}} repository.
301+
{{/reproducer_success}}
291302
"""
292303

293304

@@ -535,9 +546,13 @@ class State(PackageUpdateState):
535546
attempts_remaining: int = Field(default=max_build_attempts)
536547
used_cherry_pick_workflow: bool = Field(default=False) # Track if cherry-pick was used
537548
incremental_fix_attempts: int = Field(default=0) # Track how many times we tried incremental fix
549+
build_url: str | None = Field(default=None)
550+
reproducer_success: bool = Field(default=False)
551+
reproducer_result: str | None = Field(default=None)
552+
reproducer_info: ReproducerInfo | None = Field(default=None)
538553

539554
async def run_workflow(
540-
package, dist_git_branch, upstream_patches, jira_issue, cve_id, redis_conn=None
555+
package, dist_git_branch, upstream_patches, jira_issue, cve_id, redis_conn=None, reproducer_info=None
541556
):
542557
local_tool_options["working_directory"] = None
543558

@@ -603,6 +618,9 @@ async def run_backport_agent(state):
603618
cve_id=state.cve_id,
604619
upstream_patches=state.upstream_patches,
605620
build_error=state.build_error,
621+
reproducer_success=state.reproducer_success,
622+
reproducer_result=state.reproducer_result,
623+
reproducer_info=state.reproducer_info,
606624
),
607625
),
608626
expected_output=BackportOutputSchema,
@@ -663,6 +681,9 @@ async def fix_build_error(state):
663681
cve_id=state.cve_id,
664682
upstream_patches=state.upstream_patches,
665683
build_error=state.build_error,
684+
reproducer_success=state.reproducer_success,
685+
reproducer_result=state.reproducer_result,
686+
reproducer_info=state.reproducer_info,
666687
),
667688
),
668689
expected_output=BackportOutputSchema,
@@ -736,10 +757,14 @@ async def run_build_agent(state):
736757
if build_result.success:
737758
# Build succeeded - reset incremental fix counter for potential future failures
738759
state.incremental_fix_attempts = 0
739-
return "update_release"
760+
state.build_url = build_result.url
761+
if state.reproducer_info:
762+
return "run_reproducer_test_case"
763+
else:
764+
return "update_release"
740765
if build_result.is_timeout:
741766
logger.info(f"Build timed out for {state.jira_issue}, proceeding")
742-
return "update_release"
767+
return "run_testing_farm_test"
743768
state.attempts_remaining -= 1
744769
if state.attempts_remaining <= 0:
745770
state.backport_result.success = False
@@ -878,6 +903,23 @@ async def create_merge_request_checklist(state):
878903
async def add_fusa_label(state):
879904
return await PackageUpdateStep.add_fusa_label(state, "comment_in_jira", dry_run=dry_run, gateway_tools=gateway_tools)
880905

906+
async def run_reproducer_test_case(state):
907+
state.reproducer_success, state.reproducer_result = await tasks.run_tool(
908+
"run_testing_farm_test",
909+
git_url=state.reproducer_info.git_url,
910+
git_ref=state.reproducer_info.git_ref,
911+
path_to_test=state.reproducer_info.test,
912+
package=state.build_url,
913+
compose=state.dist_git_branch.upper() + "-Nightly",
914+
available_tools=gateway_tools,
915+
)
916+
if state.reproducer_success:
917+
logger.info(f"Reproducer result: {state.reproducer_result}")
918+
return "update_release"
919+
else:
920+
logger.error(f"Reproducer failed: {state.reproducer_result}")
921+
return "run_backport_agent"
922+
881923
async def comment_in_jira(state):
882924
if dry_run:
883925
return Workflow.END
@@ -903,6 +945,7 @@ async def comment_in_jira(state):
903945
workflow.add_step("run_backport_agent", run_backport_agent)
904946
workflow.add_step("fix_build_error", fix_build_error)
905947
workflow.add_step("run_build_agent", run_build_agent)
948+
workflow.add_step("run_reproducer_test_case", run_reproducer_test_case)
906949
workflow.add_step("update_release", update_release)
907950
workflow.add_step("stage_changes", stage_changes)
908951
workflow.add_step("run_log_agent", run_log_agent)
@@ -919,6 +962,9 @@ async def comment_in_jira(state):
919962
upstream_patches=upstream_patches,
920963
jira_issue=jira_issue,
921964
cve_id=cve_id,
965+
reproducer_info=reproducer_info,
966+
reproducer_success=False,
967+
reproducer_result=None,
922968
),
923969
)
924970
return response.state
@@ -931,13 +977,24 @@ async def comment_in_jira(state):
931977
):
932978
upstream_patches = upstream_patches_raw.split(",")
933979
logger.info("Running in direct mode with environment variables")
980+
reproducer_info_repo_url = os.getenv("REPRODUCER_INFO_REPO_URL", None)
981+
reproducer_info_repo_ref = os.getenv("REPRODUCER_INFO_REPO_REF", None)
982+
reproducer_info_test = os.getenv("REPRODUCER_INFO_TEST", None)
983+
reproducer_info = None
984+
if reproducer_info_repo_url and reproducer_info_repo_ref and reproducer_info_test:
985+
reproducer_info = ReproducerInfo(
986+
git_url=reproducer_info_repo_url,
987+
git_ref=reproducer_info_repo_ref,
988+
test=reproducer_info_test,
989+
)
934990
state = await run_workflow(
935991
package=package,
936992
dist_git_branch=branch,
937993
upstream_patches=upstream_patches,
938994
jira_issue=jira_issue,
939995
cve_id=os.getenv("CVE_ID", None),
940996
redis_conn=None,
997+
reproducer_info=reproducer_info,
941998
)
942999
logger.info(f"Direct run completed: {state.backport_result.model_dump_json(indent=4)}")
9431000
return
@@ -967,7 +1024,7 @@ async def comment_in_jira(state):
9671024
logger.info(
9681025
f"Processing backport for package: {backport_data.package}, "
9691026
f"JIRA: {backport_data.jira_issue}, branch: {dist_git_branch}, "
970-
f"attempt: {task.attempts + 1}"
1027+
f"attempt: {task.attempts + 1}, reproducer info: {backport_data.reproducer_info}"
9711028
)
9721029

9731030
async def retry(task, error):
@@ -1000,6 +1057,7 @@ async def retry(task, error):
10001057
jira_issue=backport_data.jira_issue,
10011058
cve_id=backport_data.cve_id,
10021059
redis_conn=redis,
1060+
reproducer_info=backport_data.reproducer_info,
10031061
)
10041062
logger.info(
10051063
f"Backport processing completed for {backport_data.jira_issue}, " f"success: {state.backport_result.success}"

agents/build_agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def get_instructions() -> str:
3535
just return the error message. Otherwise, start with `builder-live.log` and try to identify
3636
the build failure. If not found, try the same with `root.log`. Summarize the findings
3737
and return them as `error`. If the build failed due to a build timeout, set `is_timeout` to `true` in your output.
38+
If the build succeeded, return the URL to the built package in `url`.
3839
3940
General instructions:
4041

agents/triage_agent.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,22 @@ def render_prompt(input: InputSchema) -> str:
253253
is clarification-needed
254254
* This is the correct choice when you are sure a problem exists but cannot find the solution yourself
255255
256-
2.5 Set the Jira fields as per the instructions below.
256+
2.5. If the chosen action is backport then search for the issue reproducer in the issue comments.
257+
You need to extract the following information to say you have found the issue reproducer:
258+
- Git URL to the issue reproducer; a git repo stored in https://gitlab.com/redhat/rhel/tests/
259+
- Git reference, it can be omitted and in this case you will use "main" as the reference.
260+
- Test to run, usually a path like folder/test_name
261+
- You need to say you have found the issue reproducer and provide the information in the JSON format.
262+
The JSON format should be like this:
263+
```json
264+
{
265+
"git_url": "https://gitlab.com/redhat/rhel/tests/test_repo.git",
266+
"git_ref": "main",
267+
"test": "folder/test_name"
268+
}
269+
```
270+
271+
2.6. Set the Jira fields as per the instructions below.
257272
258273
3. **No Action**
259274
A No Action decision is appropriate for issues that are NOT bugs or CVEs requiring code fixes:
@@ -410,7 +425,12 @@ async def run_triage_analysis(state):
410425
"justification": "This patch fixes the bug by doing X, Y, and Z.",
411426
"jira_issue": "RHEL-12345",
412427
"cve_id": "CVE-1234-98765",
413-
"fix_version": "rhel-X.Y.Z"
428+
"fix_version": "rhel-X.Y.Z",
429+
"reproducer_info": {{
430+
"git_url": "https://gitlab.com/redhat/rhel/tests/test_repo.git",
431+
"git_ref": "main",
432+
"test": "folder/test_name"
433+
}}
414434
}}
415435
}}
416436
```

common/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ class RebaseOutputSchema(BaseModel):
8888
# Backport Agent Schemas
8989
# ============================================================================
9090

91+
class ReproducerInfo(BaseModel):
92+
"""Data for TMT resolution."""
93+
git_url: str = Field(description="Git URL to the TMT reproducer")
94+
git_ref: str = Field(description="Git reference to the TMT reproducer")
95+
test: str = Field(description="Test to run the TMT reproducer")
96+
9197
class BackportInputSchema(BaseModel):
9298
"""Input schema for the backport agent."""
9399
local_clone: Path = Field(description="Path to the local clone of forked dist-git repository")
@@ -99,6 +105,9 @@ class BackportInputSchema(BaseModel):
99105
upstream_patches: list[str] = Field(
100106
description="List of URLs to upstream patches that were validated using the PatchValidator tool")
101107
build_error: str | None = Field(description="Error encountered during package build")
108+
reproducer_info: ReproducerInfo | None = Field(description="Information about the TMT reproducer", default=None)
109+
reproducer_success: bool = Field(default=False, description="Whether the reproducer test case passed")
110+
reproducer_result: str | None = Field(description="Result of the reproducer test case", default=None)
102111

103112

104113
class BackportOutputSchema(BaseModel):
@@ -129,7 +138,6 @@ class RebaseData(BaseModel):
129138
jira_issue: str = Field(description="Jira issue identifier")
130139
fix_version: str | None = Field(description="Fix version in Jira (e.g., 'rhel-9.8')", default=None)
131140

132-
133141
class BackportData(BaseModel):
134142
"""Data for backport resolution."""
135143
package: str = Field(description="Package name")
@@ -139,6 +147,7 @@ class BackportData(BaseModel):
139147
jira_issue: str = Field(description="Jira issue identifier")
140148
cve_id: str | None = Field(description="CVE identifier", default=None)
141149
fix_version: str | None = Field(description="Fix version in Jira (e.g., 'rhel-9.8')", default=None)
150+
reproducer_info: ReproducerInfo | None = Field(description="Reproducer information", default=None)
142151

143152

144153
class ClarificationNeededData(BaseModel):
@@ -246,6 +255,7 @@ class BuildInputSchema(BaseModel):
246255
class BuildOutputSchema(BaseModel):
247256
"""Output schema for the build agent."""
248257
success: bool = Field(description="Whether the build was successfully completed")
258+
url: str | None = Field(description="URL to the built package", default=None)
249259
error: str | None = Field(description="Specific details about an error")
250260
is_timeout: bool = Field(default=False, description="Whether the build failed due to a timeout")
251261

mcp_server/gateway.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
import gitlab_tools
1010
import jira_tools
1111
import lookaside_tools
12+
import testing_farm_tools
1213

1314

1415
mcp = FastMCP(
1516
name="MCP Gateway",
1617
tools=[
1718
coroutine
18-
for module in [copr_tools, distgit_tools, gitlab_tools, jira_tools, lookaside_tools]
19+
for module in [copr_tools, distgit_tools, gitlab_tools, jira_tools, lookaside_tools, testing_farm_tools]
1920
for name, coroutine in inspect.getmembers(module, inspect.iscoroutinefunction)
2021
if coroutine.__module__ == module.__name__
2122
and not name.startswith("_")

mcp_server/testing_farm_tools.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import asyncio
2+
import logging
3+
import shlex
4+
from typing import Annotated, Tuple
5+
6+
from fastmcp.exceptions import ToolError
7+
from pydantic import Field
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
async def run_testing_farm_test(
13+
git_url: Annotated[str, Field(description="Git URL to the test repository")],
14+
git_ref: Annotated[str, Field(description="Git reference to the test repository")],
15+
path_to_test: Annotated[str, Field(description="Path to the test to run")],
16+
package: Annotated[str, Field(description="Package URL to be installed in the test environment")],
17+
compose: Annotated[str, Field(description="Testing Farm compose to use")],
18+
) -> Tuple[bool, str]:
19+
"""Runs the specified testing-farm test and returns True if the test passed, False otherwise."""
20+
21+
tmt_prepare = f'--insert --how install --package {shlex.quote(package)}'
22+
23+
# Build the command arguments
24+
cmd = [
25+
"testing-farm",
26+
"request",
27+
"--tmt-prepare", tmt_prepare,
28+
"--compose", compose,
29+
"--git-ref", git_ref,
30+
"--git-url", git_url,
31+
"--test", path_to_test,
32+
]
33+
34+
logger.info(f"Running testing-farm command: {' '.join(shlex.quote(arg) for arg in cmd)}")
35+
36+
try:
37+
process = await asyncio.create_subprocess_exec(
38+
*cmd,
39+
stdout=asyncio.subprocess.PIPE,
40+
stderr=asyncio.subprocess.PIPE,
41+
)
42+
43+
stdout, stderr = await process.communicate()
44+
45+
if process.returncode == 0:
46+
msg = f"Testing Farm test passed: \n stdout {'='*60}\n {stdout.decode()}\n {'='*60}\n stderr {'='*60}\n {stderr.decode()}\n {'='*60}"
47+
logger.info(msg)
48+
return True, msg
49+
else:
50+
msg = f"Testing Farm test failed (exit code {process.returncode}): \n stdout {'='*60}\n {stdout.decode()}\n {'='*60}\n stderr {'='*60}\n {stderr.decode()}\n {'='*60}"
51+
logger.error(msg)
52+
return False, msg
53+
54+
except Exception as e:
55+
logger.error(f"Failed to run testing-farm command: {e}")
56+
raise ToolError(f"Failed to run testing-farm test: {e}") from e

templates/mcp-gateway.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ GITLAB_TOKEN=
66
JIRA_URL=https://issues.redhat.com
77
# JIRA profile -> Create token
88
JIRA_TOKEN=
9+
10+
TESTING_FARM_API_TOKEN=

0 commit comments

Comments
 (0)