Skip to content

Commit 0bdb015

Browse files
committed
Run a reproducer in a simple way
1 parent 9906236 commit 0bdb015

8 files changed

Lines changed: 154 additions & 7 deletions

File tree

Containerfile.mcp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ RUN dnf -y install \
1616
krb5-workstation \
1717
centpkg \
1818
git \
19+
testing-farm \
1920
&& dnf clean all
2021

2122
# 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: 58 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,12 @@ 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_failed}}
293+
The reproducer failed after the first backport attempt.
294+
Keep it in mind and try to fix the patches you apply to make the reproducer pass.
295+
You can inspect the reproducer test at {{tmt_reproducer.git_url}}/blob/{{tmt_reproducer.git_ref}}/{{tmt_reproducer.test}}.
296+
The test is located in the {{tmt_reproducer.git_ref}} branch of the {{tmt_reproducer.git_url}} repository.
297+
{{/reproducer_failed}}
291298
"""
292299

293300

@@ -535,9 +542,11 @@ class State(PackageUpdateState):
535542
attempts_remaining: int = Field(default=max_build_attempts)
536543
used_cherry_pick_workflow: bool = Field(default=False) # Track if cherry-pick was used
537544
incremental_fix_attempts: int = Field(default=0) # Track how many times we tried incremental fix
545+
build_url: str | None = Field(default=None)
546+
reproducer_failed: bool = Field(default=False)
538547

539548
async def run_workflow(
540-
package, dist_git_branch, upstream_patches, jira_issue, cve_id, redis_conn=None
549+
package, dist_git_branch, upstream_patches, jira_issue, cve_id, redis_conn=None, reproducer_info=None
541550
):
542551
local_tool_options["working_directory"] = None
543552

@@ -603,6 +612,7 @@ async def run_backport_agent(state):
603612
cve_id=state.cve_id,
604613
upstream_patches=state.upstream_patches,
605614
build_error=state.build_error,
615+
reproducer_failed=state.reproducer_failed,
606616
),
607617
),
608618
expected_output=BackportOutputSchema,
@@ -663,6 +673,7 @@ async def fix_build_error(state):
663673
cve_id=state.cve_id,
664674
upstream_patches=state.upstream_patches,
665675
build_error=state.build_error,
676+
reproducer_failed=state.reproducer_failed,
666677
),
667678
),
668679
expected_output=BackportOutputSchema,
@@ -736,10 +747,14 @@ async def run_build_agent(state):
736747
if build_result.success:
737748
# Build succeeded - reset incremental fix counter for potential future failures
738749
state.incremental_fix_attempts = 0
739-
return "update_release"
750+
state.build_url = build_result.url
751+
if state.reproducer_info:
752+
return "run_reproducer"
753+
else:
754+
return "update_release"
740755
if build_result.is_timeout:
741756
logger.info(f"Build timed out for {state.jira_issue}, proceeding")
742-
return "update_release"
757+
return "run_testing_farm_test"
743758
state.attempts_remaining -= 1
744759
if state.attempts_remaining <= 0:
745760
state.backport_result.success = False
@@ -878,6 +893,30 @@ async def create_merge_request_checklist(state):
878893
async def add_fusa_label(state):
879894
return await PackageUpdateStep.add_fusa_label(state, "comment_in_jira", dry_run=dry_run, gateway_tools=gateway_tools)
880895

896+
async def run_reproducer(state):
897+
reproducer_result = await tasks.run_tool(
898+
"run_testing_farm_test",
899+
git_url=state.reproducer_info.git_url,
900+
git_ref=state.reproducer_info.git_ref,
901+
path_to_test=state.reproducer_info.test,
902+
package=state.build_url,
903+
compose=state.dist_git_branch.upper() + "Nightly",
904+
available_tools=gateway_tools,
905+
)
906+
if reproducer_result:
907+
return "update_release"
908+
else:
909+
state.reproducer_failed = True
910+
state.attempts_remaining -= 1
911+
if state.attempts_remaining <= 0:
912+
state.backport_result.success = False
913+
state.backport_result.error = (
914+
f"Unable to successfully run the reproducer in {max_build_attempts} attempts"
915+
)
916+
return "comment_in_jira"
917+
# Run the backport agent again to try to fix the reproducer error
918+
return "run_backport_agent"
919+
881920
async def comment_in_jira(state):
882921
if dry_run:
883922
return Workflow.END
@@ -903,6 +942,7 @@ async def comment_in_jira(state):
903942
workflow.add_step("run_backport_agent", run_backport_agent)
904943
workflow.add_step("fix_build_error", fix_build_error)
905944
workflow.add_step("run_build_agent", run_build_agent)
945+
workflow.add_step("run_reproducer", run_reproducer)
906946
workflow.add_step("update_release", update_release)
907947
workflow.add_step("stage_changes", stage_changes)
908948
workflow.add_step("run_log_agent", run_log_agent)
@@ -919,6 +959,8 @@ async def comment_in_jira(state):
919959
upstream_patches=upstream_patches,
920960
jira_issue=jira_issue,
921961
cve_id=cve_id,
962+
reproducer_info=reproducer_info,
963+
reproducer_failed=False,
922964
),
923965
)
924966
return response.state
@@ -931,13 +973,24 @@ async def comment_in_jira(state):
931973
):
932974
upstream_patches = upstream_patches_raw.split(",")
933975
logger.info("Running in direct mode with environment variables")
976+
reproducer_info_repo_url = os.getenv("REPRODUCER_INFO_REPO_URL", None)
977+
reproducer_info_repo_ref = os.getenv("REPRODUCER_INFO_REPO_REF", None)
978+
reproducer_info_test = os.getenv("REPRODUCER_INFO_TEST", None)
979+
reproducer_info = None
980+
if reproducer_info_repo_url and reproducer_info_repo_ref and reproducer_info_test:
981+
reproducer_info = ReproducerInfo(
982+
git_url=reproducer_info_repo_url,
983+
git_ref=reproducer_info_repo_ref,
984+
test=reproducer_info_test,
985+
)
934986
state = await run_workflow(
935987
package=package,
936988
dist_git_branch=branch,
937989
upstream_patches=upstream_patches,
938990
jira_issue=jira_issue,
939991
cve_id=os.getenv("CVE_ID", None),
940992
redis_conn=None,
993+
reproducer_info=reproducer_info,
941994
)
942995
logger.info(f"Direct run completed: {state.backport_result.model_dump_json(indent=4)}")
943996
return
@@ -967,7 +1020,7 @@ async def comment_in_jira(state):
9671020
logger.info(
9681021
f"Processing backport for package: {backport_data.package}, "
9691022
f"JIRA: {backport_data.jira_issue}, branch: {dist_git_branch}, "
970-
f"attempt: {task.attempts + 1}"
1023+
f"attempt: {task.attempts + 1}, reproducer info: {backport_data.reproducer_info}"
9711024
)
9721025

9731026
async def retry(task, error):
@@ -1000,6 +1053,7 @@ async def retry(task, error):
10001053
jira_issue=backport_data.jira_issue,
10011054
cve_id=backport_data.cve_id,
10021055
redis_conn=redis,
1056+
reproducer_info=backport_data.reproducer_info,
10031057
)
10041058
logger.info(
10051059
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: 10 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,8 @@ 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_failed: bool = Field(default=False, description="Whether the reproducer failed")
102110

103111

104112
class BackportOutputSchema(BaseModel):
@@ -129,7 +137,6 @@ class RebaseData(BaseModel):
129137
jira_issue: str = Field(description="Jira issue identifier")
130138
fix_version: str | None = Field(description="Fix version in Jira (e.g., 'rhel-9.8')", default=None)
131139

132-
133140
class BackportData(BaseModel):
134141
"""Data for backport resolution."""
135142
package: str = Field(description="Package name")
@@ -139,6 +146,7 @@ class BackportData(BaseModel):
139146
jira_issue: str = Field(description="Jira issue identifier")
140147
cve_id: str | None = Field(description="CVE identifier", default=None)
141148
fix_version: str | None = Field(description="Fix version in Jira (e.g., 'rhel-9.8')", default=None)
149+
reproducer_info: ReproducerInfo | None = Field(description="Reproducer information", default=None)
142150

143151

144152
class ClarificationNeededData(BaseModel):
@@ -246,6 +254,7 @@ class BuildInputSchema(BaseModel):
246254
class BuildOutputSchema(BaseModel):
247255
"""Output schema for the build agent."""
248256
success: bool = Field(description="Whether the build was successfully completed")
257+
url: str | None = Field(description="URL to the built package", default=None)
249258
error: str | None = Field(description="Specific details about an error")
250259
is_timeout: bool = Field(default=False, description="Whether the build failed due to a timeout")
251260

mcp_server/testing_farm_tools.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import asyncio
2+
import logging
3+
import shlex
4+
from typing import Annotated
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+
) -> bool:
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+
logger.info(f"Testing Farm test passed: {stdout.decode()}")
47+
return True
48+
else:
49+
logger.error(f"Testing Farm test failed (exit code {process.returncode}): {stderr.decode()}")
50+
return False
51+
52+
except Exception as e:
53+
logger.error(f"Failed to run testing-farm command: {e}")
54+
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)