From ee461170a94cf4a7337a8d990ac9a1401409bb22 Mon Sep 17 00:00:00 2001 From: Jan Matufka Date: Wed, 23 Sep 2026 14:40:06 +0200 Subject: [PATCH] Fix LD reporting for noarch packages Child buildArch subtask triggered from Koji scratch build for a noarch package can be run on any architecture - noarch, x86_64, aarch64, etc. This means that retriggers can pick a different arch than the job before, which breaks the CI reporting - two different Log Detective analysis jobs are created, when the more recent one should have replaced the old one. Assisted-by: Codex gpt-5.6-terra Signed-off-by: Jan Matufka --- packit_service/events/koji/result.py | 3 ++ packit_service/worker/helpers/logdetective.py | 14 +++++- packit_service/worker/parser.py | 3 ++ tests/unit/events/test_koji.py | 18 ++++++++ tests/unit/test_logdetective_koji_helper.py | 46 +++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packit_service/events/koji/result.py b/packit_service/events/koji/result.py index b5da7782b..4d93bac73 100644 --- a/packit_service/events/koji/result.py +++ b/packit_service/events/koji/result.py @@ -154,6 +154,7 @@ def __init__( old_state: Optional[KojiTaskState] = None, rpm_build_task_ids: Optional[dict[str, int]] = None, rpm_build_failed_arch_list: Optional[list[str]] = None, + rpm_build_task_labels: Optional[dict[str, str]] = None, start_time: Optional[Union[int, float, str]] = None, completion_time: Optional[Union[int, float, str]] = None, ): @@ -164,6 +165,7 @@ def __init__( completion_time=completion_time, ) self.rpm_build_failed_arch_list = rpm_build_failed_arch_list + self.rpm_build_task_labels = rpm_build_task_labels or {} self.state = state self.old_state = old_state @@ -227,6 +229,7 @@ def from_event_dict(cls, event: dict) -> "Task": old_state=(KojiTaskState(event.get("old_state")) if event.get("old_state") else None), rpm_build_task_ids=event.get("rpm_build_task_ids"), rpm_build_failed_arch_list=event.get("rpm_build_failed_arch_list"), + rpm_build_task_labels=event.get("rpm_build_task_labels"), start_time=event.get("start_time"), completion_time=event.get("completion_time"), ) diff --git a/packit_service/worker/helpers/logdetective.py b/packit_service/worker/helpers/logdetective.py index 282643c5b..123fa19f7 100644 --- a/packit_service/worker/helpers/logdetective.py +++ b/packit_service/worker/helpers/logdetective.py @@ -215,10 +215,22 @@ def trigger_log_detective_analysis_for_arch(self, arch: str) -> bool: # "target" field in LDRunModel refers to: # - "target-arch" for Koji builds (e.g. fc44-aarch64) # - "chroot" for Copr builds (e.g. fedora-rawhide-x86_64) + + # Note: Parent tasks for scratch builds of both arch-sensitive and arch-agnostic (noarch) + # packages have arch=="noarch". + # - A) In case of noarch packages, child buildArch subtasks have "noarch" label. + # So parent task: arch=noarch, child buildArch task arch=whatever, but label=noarch. + # - B) In case of arch-sensitive buildArch subtasks, label is the same as arch. + # Since koji can give builders with different arches for noarch packages when retriggering, + # we have to use label to ensure that the new analysis CI job replaces the old one. + + status_arch = ( + "noarch" if self.koji_event.rpm_build_task_labels.get(arch) == "noarch" else arch + ) LogDetectiveRunModel.create( LogDetectiveResult.running, str(build_arch_task_id), - f"{self.koji_event.target}-{arch}", + f"{self.koji_event.target}-{status_arch}", LogDetectiveBuildSystem.koji, analysis_id, self.run_group, diff --git a/packit_service/worker/parser.py b/packit_service/worker/parser.py index 55508f812..37d5a7a1f 100644 --- a/packit_service/worker/parser.py +++ b/packit_service/worker/parser.py @@ -1458,12 +1458,14 @@ def parse_koji_task_event(event) -> Optional[koji.result.Task]: rpm_build_task_ids = {} rpm_build_failed_arch_list: list[str] = [] + rpm_build_task_labels = {} for children in nested_get(event, "info", "children", default=[]): arch = children.get("arch") subtask_id = children.get("id") subtask_state = children.get("state") if children.get("method") == "buildArch": rpm_build_task_ids[arch] = subtask_id + rpm_build_task_labels[arch] = children.get("label") if KojiTaskState.from_number(subtask_state) == KojiTaskState.failed: rpm_build_failed_arch_list.append(arch) @@ -1475,6 +1477,7 @@ def parse_koji_task_event(event) -> Optional[koji.result.Task]: completion_time=completion_time, rpm_build_task_ids=rpm_build_task_ids, rpm_build_failed_arch_list=rpm_build_failed_arch_list, + rpm_build_task_labels=rpm_build_task_labels, ) @staticmethod diff --git a/tests/unit/events/test_koji.py b/tests/unit/events/test_koji.py index ec3926905..938d7bc0c 100644 --- a/tests/unit/events/test_koji.py +++ b/tests/unit/events/test_koji.py @@ -39,6 +39,7 @@ def test_parse_koji_build_scratch_event_end(koji_build_scratch_end, koji_build_p assert event_object.task_id == 45270170 assert event_object.state == KojiTaskState.closed assert event_object.rpm_build_task_ids == {"noarch": 45270227} + assert event_object.rpm_build_task_labels == {"noarch": "noarch"} assert event_object.get_koji_build_rpm_tasks_logs_urls() == { "noarch": "https://kojipkgs.fedoraproject.org//work/tasks/227/45270227/mock_output.log", } @@ -50,6 +51,23 @@ def test_parse_koji_build_scratch_event_end(koji_build_scratch_end, koji_build_p assert event_object.project.full_repo_name == "foo/bar" +def test_parse_koji_build_scratch_event_preserves_noarch_label( + koji_build_scratch_end, +): + """ + This is related to the broken reporting of Log Detective jobs for noarch packages. + Koji could provision different arches for different LD runs, posting 2 seemingly + different analyses, when in fact they should be replaced. The parser for koji events + had to also store labels for that reason. + """ + koji_build_scratch_end["info"]["children"][1]["arch"] = "x86_64" + + event_object = Parser.parse_koji_task_event(koji_build_scratch_end) + + assert event_object.rpm_build_task_ids == {"x86_64": 45270227} + assert event_object.rpm_build_task_labels == {"x86_64": "noarch"} + + def test_parse_koji_build_event_start_old_format( koji_build_start_old_format, mock_config, diff --git a/tests/unit/test_logdetective_koji_helper.py b/tests/unit/test_logdetective_koji_helper.py index 69dcca197..4b94602d6 100644 --- a/tests/unit/test_logdetective_koji_helper.py +++ b/tests/unit/test_logdetective_koji_helper.py @@ -74,6 +74,7 @@ def mock_koji_task_failed_event(): build_model=mock_build_model, rpm_build_task_ids={"x86_64": 12345}, rpm_build_failed_arch_list=["x86_64"], + rpm_build_task_labels={"x86_64": "x86_64"}, db_project_object=None, start_time=1000, completion_time=1045, @@ -221,6 +222,51 @@ def test_logdetective_koji_success( assert all(trigger_success_list) +def test_logdetective_koji_noarch_build_uses_noarch_status_target( + mock_koji_task_failed_event, mock_event_data, mock_pushgateway_log_detective_inc +): + mock_response = flexmock(status_code=200) + mock_response.should_receive("raise_for_status") + mock_response.should_receive("json").and_return( + { + "log_detective_analysis_id": "test-uuid-123", + "creation_time": "2026-01-01T12:00:00", + } + ) + mock_koji_task_failed_event.rpm_build_task_labels = {"x86_64": "noarch"} + flexmock(logdetective_module).should_receive("verify_artifact").and_return(True) + flexmock(requests).should_receive("post").once().and_return(mock_response) + + mock_group_run = flexmock() + flexmock(LogDetectiveRunGroupModel).should_receive("create").once().and_return(mock_group_run) + flexmock(LogDetectiveRunModel).should_receive("create").with_args( + LogDetectiveResult.running, + "12345", + "rawhide-noarch", + LogDetectiveBuildSystem.koji, + "test-uuid-123", + mock_group_run, + ).once() + mock_koji_task_failed_event.build_model.should_receive("add_log_detective_run").with_args( + "test-uuid-123" + ).once() + flexmock(logger).should_receive("info").with_args( + "Triggered Log Detective for a failed Koji build " + "(child taskID = 12345, arch = x86_64, trigger = success)" + ) + + helper = LogDetectiveKojiTriggerHelper( + mock_koji_task_failed_event, + mock_event_data, + mock_pushgateway_log_detective_inc, + "https://kojipkgs.fedoraproject.org", + LOGDETECTIVE_PACKIT_SERVER_URL, + "secret-123", + ) + + assert helper.trigger_log_detective_analysis() == [True] + + def test_logdetective_koji_http_error( mock_koji_task_failed_event, mock_event_data, mock_pushgateway_log_detective_no_inc ):