Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions packit_service/worker/helpers/logdetective.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,19 @@
from packit_service.events import koji
from packit_service.events.event_data import EventData
from packit_service.models import (
GitBranchModel,
LogDetectiveBuildSystem,
LogDetectiveResult,
LogDetectiveRunGroupModel,
LogDetectiveRunModel,
ProjectReleaseModel,
PullRequestModel,
)
from packit_service.utils import verify_artifact
from packit_service.worker.monitoring import Pushgateway

logger = logging.getLogger(__name__)

LD_COMMENTARY = (
"Build was executed in downstream Koji using containerized environment provided by Mock."
"The build.log contains output of the package build, it is the most likely to contain messages,"
" indicating the root cause."
"The mock_output.log is a general log from Mock."
"The root.log is a log from creation of the chroot environment."
)


class LogDetectiveKojiTriggerHelper:
"""
Expand Down Expand Up @@ -65,6 +60,55 @@ def __init__(
# run_group created after 1st succcessful trigger, right before creating RunModel
self.run_group: Optional[LogDetectiveRunGroupModel] = None

def _format_duration(self) -> str:
"""Return a human-readable build duration, or empty string if unavailable."""
try:
start = float(self.koji_event.start_time)
end = float(self.koji_event.completion_time)
except (TypeError, ValueError):
return ""
seconds = end - start
if seconds < 0:
return ""
return f"Build ran for {seconds:.0f} seconds before failing."

def _build_commentary(self, arch: str) -> str:
"""Build a dynamic commentary string with per-build context for Log Detective."""
build = self.koji_event.build_model
parts = [
"Build was executed in downstream Koji"
" using containerized environment provided by Mock.",
f"Package NVR: {build.nvr or 'unknown'},"
f" target: {self.koji_event.target or 'unknown'}, arch: {arch}.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] edge-case

_build_commentary accesses self.koji_event.build_model and immediately dereferences build.nvr without a None guard. If build_model were None, this would raise AttributeError. However, this mirrors the existing pattern in trigger_log_detective_analysis_for_arch and is not practically reachable — the handler returns early if the build model is absent.

"Scratch build." if build.scratch else "Official (non-scratch) build.",
]
db_project_object = self.koji_event.db_project_object
if isinstance(db_project_object, PullRequestModel):
parts.append(f"PR build (PR #{db_project_object.pr_id}).")
elif isinstance(db_project_object, GitBranchModel):
parts.append(f"Branch build ({db_project_object.name}).")
elif isinstance(db_project_object, ProjectReleaseModel):
parts.append(f"Release build (tag: {db_project_object.tag_name}).")
duration = self._format_duration()
if duration:
parts.append(duration)
if build.sidetag:
parts.append(
f"Built in sidetag: {build.sidetag}."
" Sidetag builds use an isolated buildroot inheriting from the base tag;"
" dependency resolution failures may reflect non-default package versions"
" present in the sidetag."
)
parts += [
"The build.log contains output of the package build"
" and is the most likely source of the root cause.",
"The mock_output.log is a general log from Mock.",
"The root.log is a log from creation of the chroot environment.",
]
if build.build_submission_stdout:
parts.append(f"Build submission output: {build.build_submission_stdout}")
return " ".join(parts)

def trigger_log_detective_analysis(self) -> list[bool]:
"""
Run a trigger over all arches for which we have a failed buildArch task.
Expand Down Expand Up @@ -121,7 +165,7 @@ def trigger_log_detective_analysis_for_arch(self, arch: str) -> bool:
endpoint_url = f"{self.url}/analyze"
request_json = {
"artifacts": artifacts,
"build_metadata": {"commentary": LD_COMMENTARY},
"build_metadata": {"commentary": self._build_commentary(arch)},
"target_build": str(build_arch_task_id),
"build_system": LogDetectiveBuildSystem.koji.value,
"commit_sha": self.data.commit_sha,
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/test_logdetective_koji.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ def test_logdetective_koji_build_scratch_downstream(
flexmock(ServiceConfig).should_receive("get_service_config").and_return(service_config)

koji_build_pr_downstream.target = "rawhide"
koji_build_pr_downstream.nvr = "packit-0.123.0-1.fc00"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-inadequate

The integration test sets new attributes (build_submission_stdout, nvr, scratch, sidetag) on koji_build_pr_downstream but the requests.post mock accepts any arguments, so the dynamic commentary payload is never validated end-to-end. Additionally, db_project_object resolves to a flexmock that will fail isinstance(db_project_object, PullRequestModel) (flexmock objects are not instances of the real model class), causing the PR-specific commentary line to be silently skipped.

Suggested fix: Add a payload assertion on the requests.post mock to verify the commentary string, or use flexmock(PullRequestModel) to ensure isinstance checks work correctly in tests.

koji_build_pr_downstream.scratch = True
koji_build_pr_downstream.sidetag = None
koji_build_pr_downstream.build_submission_stdout = "MOCK STDOUT"
flexmock(koji.result.Task).should_receive("get_packages_config").and_return(None)
flexmock(KojiBuildTargetModel).should_receive("get_by_task_id").and_return(
koji_build_pr_downstream
Expand Down
Loading
Loading