Repository: packit/packit

Issue:
Implement cancelling of the Koji jobs
Follow-up to #2534

As of now:

- upstream (scratch) Koji builds are not that widely adopted
- downstream Koji builds are run (by default) only on Packit-authored commits or PRs, hence the chance of running them by mistake is lower

However running Koji builds for PRs without cancelling from the perspective of Fedora CI, is wasteful. Therefore implement the Packit API for cancelling Koji builds too. There are 3 different API calls that allow cancelling of Koji… _something_.

API methods:
- cancelling a specific build - [cancelBuild](https://koji.fedoraproject.org/koji/api#:~:text=cancelBuild(buildID%2C%20strict%3DFalse))
- cancelling a task - [cancelTask](https://koji.fedoraproject.org/koji/api#:~:text=cancelTask(task_id%2C%20recurse%3DTrue))
- cancelling a “full” task - [cancelTaskFull](https://koji.fedoraproject.org/koji/api#:~:text=cancelTaskFull(task_id%2C%20strict%3DTrue))

TODO:

- [ ] Try to map the Koji API methods to Packit use cases (scratch and non-scratch / production builds)
- [ ] Implement needed methods in the API

Discussion hints:

Solution patch:
diff --git a/packit/utils/koji_helper.py b/packit/utils/koji_helper.py
index 7b61a64b0..94a854de9 100644
--- a/packit/utils/koji_helper.py
+++ b/packit/utils/koji_helper.py
@@ -398,6 +398,24 @@ def untag_build(self, nvr: str, tag: str) -> None:
         except Exception as e:
             logger.debug(f"Failed to untag {nvr} from {tag} in Koji: {e}")
 
+    def cancel_task(self, task_id: int) -> bool:
+        """
+        Cancels a task and its children recursively.
+
+        Args:
+            task_id: Koji task ID.
+
+        Returns:
+            Whether the cancellation was successful.
+        """
+        logger.info(f"Cancelling Koji task with ID {task_id}")
+        try:
+            self.session.cancelTask(task_id, recurse=True)
+            return True
+        except Exception as e:
+            logger.debug(f"Failed to cancel Koji task {task_id}: {e}")
+            return False
+
     def get_build_target(self, dist_git_branch: str) -> Optional[dict]:
         """
         Gets a build target from a dist-git branch name.


Test patch:
diff --git a/tests/unit/utils/test_koji_helper.py b/tests/unit/utils/test_koji_helper.py
index 720ce1b83..21c672fe6 100644
--- a/tests/unit/utils/test_koji_helper.py
+++ b/tests/unit/utils/test_koji_helper.py
@@ -301,6 +301,28 @@ def untagBuild(*_, **__):
     KojiHelper().untag_build("test-1.0-1.fc39", "f39-build-side-12345")
 
 
+@pytest.mark.parametrize(
+    "logged_in, error",
+    [(True, False), (False, False), (True, True)],
+)
+def test_cancel_task(logged_in, error):
+    @koji_session_virtual_method(requires_authentication=not logged_in)
+    def cancelTask(*_, **__):
+        if error:
+            raise Exception
+
+    session = flexmock(cancelTask=cancelTask)
+    session.should_receive("gssapi_login").times(
+        0 if logged_in else 1,
+    )
+    flexmock(ClientSession).new_instances(session)
+    result = KojiHelper().cancel_task(12345)
+    if error:
+        assert result is False
+    else:
+        assert result is True
+
+
 @pytest.mark.parametrize(
     "error",
     [False, True],