Skip to content

Commit d52a969

Browse files
Koji builds cancellation (#3000)
Koji builds cancellation TODO: Write new tests or update the old ones to cover new functionality. Cancel upstream builds before starting new ones Cancel downstream Koji scratch builds before starting new ones Cancel downstream production builds Add integration tests, ideally for all cases Fixes #2989 RELEASE NOTES BEGIN Packit now cancels obsoleted Koji builds before starting new ones. RELEASE NOTES END Reviewed-by: gemini-code-assist[bot] Reviewed-by: Nikola Forró Reviewed-by: Marek Blaha <mblaha@redhat.com> Reviewed-by: Matej Focko
2 parents 623822f + 347c598 commit d52a969

File tree

9 files changed

+625
-151
lines changed

9 files changed

+625
-151
lines changed

packit_service/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,34 @@ def create(cls, run_model: "PipelineModel") -> "KojiBuildGroupModel":
26262626
session.add(run_model)
26272627
return build_group
26282628

2629+
@classmethod
2630+
def get_running(
2631+
cls,
2632+
project_event_type: ProjectEventModelType,
2633+
event_id: int,
2634+
) -> Iterable["KojiBuildTargetModel"]:
2635+
"""Get running Koji builds for a given project object (e.g. a PR or branch).
2636+
2637+
Args:
2638+
project_event_type: Type of the project event (e.g. pull_request).
2639+
event_id: ID of the project object (e.g. PullRequestModel.id).
2640+
2641+
Returns:
2642+
An iterable over KojiBuildTargetModels in non-final states.
2643+
"""
2644+
with sa_session_transaction() as session:
2645+
return (
2646+
session.query(KojiBuildTargetModel)
2647+
.join(KojiBuildGroupModel)
2648+
.join(PipelineModel)
2649+
.join(ProjectEventModel)
2650+
.filter(
2651+
ProjectEventModel.type == project_event_type,
2652+
ProjectEventModel.event_id == event_id,
2653+
KojiBuildTargetModel.status.in_(("pending", "queued", "running")),
2654+
)
2655+
)
2656+
26292657

26302658
class BodhiUpdateTargetModel(GroupAndTargetModelConnector, Base):
26312659
__tablename__ = "bodhi_update_targets"

packit_service/worker/handlers/distgit.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@
9999
run_for_comment,
100100
run_for_comment_as_fedora_ci,
101101
)
102-
from packit_service.worker.handlers.mixin import GetProjectToSyncMixin
102+
from packit_service.worker.handlers.mixin import (
103+
GetKojiBuildJobHelperMixin,
104+
GetProjectToSyncMixin,
105+
)
103106
from packit_service.worker.helpers.fedora_ci import FedoraCIHelper
104107
from packit_service.worker.helpers.sidetag import SidetagHelper
105108
from packit_service.worker.helpers.sync_release.propose_downstream import (
@@ -779,6 +782,7 @@ class DownstreamKojiScratchBuildHandler(
779782
ConfigFromUrlMixin,
780783
LocalProjectMixin,
781784
PackitAPIWithDownstreamMixin,
785+
GetKojiBuildJobHelperMixin,
782786
):
783787
"""
784788
This handler can submit a scratch build in Koji from a dist-git (Fedora CI).
@@ -880,6 +884,15 @@ def report(self, description: str, commit_status: BaseCommitStatus, url: Optiona
880884
)
881885

882886
def _run(self) -> TaskResults:
887+
if getenv("CANCEL_RUNNING_JOBS"):
888+
self.koji_build_helper.cancel_running_builds(
889+
report_canceled=lambda build: self.report(
890+
commit_status=BaseCommitStatus.canceled,
891+
description="Build was canceled.",
892+
url=get_koji_build_info_url(build.id),
893+
),
894+
)
895+
883896
try:
884897
self.packit_api.init_kerberos_ticket()
885898
except PackitCommandFailedError as ex:
@@ -989,6 +1002,7 @@ def get_checkers() -> tuple[type[Checker], ...]:
9891002
class AbstractDownstreamKojiBuildHandler(
9901003
abc.ABC,
9911004
RetriableJobHandler,
1005+
GetKojiBuildJobHelperMixin,
9921006
):
9931007
"""
9941008
This handler can submit a build in Koji from a dist-git.
@@ -1100,6 +1114,9 @@ def is_already_triggered(self, nvr: str) -> bool:
11001114
return False
11011115

11021116
def _run(self) -> TaskResults:
1117+
if getenv("CANCEL_RUNNING_JOBS"):
1118+
self.koji_build_helper.cancel_running_builds()
1119+
11031120
try:
11041121
group = self._get_or_create_koji_group_model()
11051122
except PackitException as ex:

packit_service/worker/handlers/koji.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
class KojiBuildHandler(
9696
JobHandler,
9797
PackitAPIWithDownstreamMixin,
98+
ConfigFromEventMixin,
9899
GetKojiBuildJobHelperMixin,
99100
):
100101
task_name = TaskName.upstream_koji_build
@@ -123,6 +124,18 @@ def get_checkers() -> tuple[type[Checker], ...]:
123124
)
124125

125126
def _run(self) -> TaskResults:
127+
if getenv("CANCEL_RUNNING_JOBS"):
128+
self.koji_build_helper.cancel_running_builds(
129+
report_canceled=(
130+
lambda build: self.koji_build_helper.report_status_to_build_for_chroot(
131+
state=BaseCommitStatus.canceled,
132+
description="Build was canceled.",
133+
url=get_koji_build_info_url(build.id),
134+
chroot=build.target,
135+
)
136+
)
137+
)
138+
126139
return self.koji_build_helper.run_koji_build()
127140

128141

packit_service/worker/handlers/mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class GetKojiBuildJobHelper(Protocol):
148148
def koji_build_helper(self) -> KojiBuildJobHelper: ...
149149

150150

151-
class GetKojiBuildJobHelperMixin(GetKojiBuildJobHelper, ConfigFromEventMixin):
151+
class GetKojiBuildJobHelperMixin(GetKojiBuildJobHelper, Config):
152152
_koji_build_helper: Optional[KojiBuildJobHelper] = None
153153
package_config: PackageConfig
154154
job_config: JobConfig

packit_service/worker/helpers/build/koji_build.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
# SPDX-License-Identifier: MIT
33

44
import logging
5-
from collections.abc import Iterable
6-
from typing import Any, Optional
5+
from collections.abc import Callable, Iterable
6+
from typing import Optional
77

88
from ogr.abstract import GitProject
99
from packit.config import JobConfig, JobType
1010
from packit.config.aliases import get_all_koji_targets, get_koji_targets
1111
from packit.config.package_config import PackageConfig
1212
from packit.exceptions import PackitCommandFailedError
13+
from packit.utils.koji_helper import KojiHelper
1314

1415
from packit_service import sentry_integration
1516
from packit_service.config import ServiceConfig
@@ -227,6 +228,33 @@ def run_build(
227228

228229
return get_koji_task_id_and_url_from_stdout(out)
229230

230-
# [TODO] Switch from ‹Any› to the correct type when implementing
231-
def get_running_jobs(self) -> Iterable[Any]:
232-
raise NotImplementedError("See https://github.com/packit/packit/issues/2535")
231+
def get_running_jobs(self) -> Iterable[KojiBuildTargetModel]:
232+
return KojiBuildGroupModel.get_running(
233+
project_event_type=self.db_project_event.type,
234+
event_id=self.db_project_event.event_id,
235+
)
236+
237+
def cancel_running_builds(
238+
self,
239+
report_canceled: Optional[Callable[[KojiBuildTargetModel], None]] = None,
240+
) -> None:
241+
"""Cancel running Koji builds for the current project object
242+
(e.g. a PR or branch).
243+
244+
Args:
245+
report_canceled: Optional callback to report the cancellation
246+
to the forge. Called with each canceled build model.
247+
"""
248+
running_builds = list(self.get_running_jobs())
249+
if not running_builds:
250+
logger.info("No running Koji builds to cancel.")
251+
return
252+
253+
koji_helper = KojiHelper()
254+
for build in running_builds:
255+
if build.task_id is not None:
256+
logger.debug(f"Cancelling Koji task {build.task_id}")
257+
koji_helper.cancel_task(int(build.task_id))
258+
build.set_status(BuildStatus.canceled)
259+
if report_canceled:
260+
report_canceled(build)

tests/integration/conftest.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
import pytest
5+
from flexmock import flexmock
6+
from github.MainClass import Github
7+
from ogr.services.github import GithubProject
8+
from packit.config import JobConfigTriggerType
9+
from packit.local_project import LocalProject
10+
11+
from packit_service.models import (
12+
GitBranchModel,
13+
ProjectEventModel,
14+
ProjectReleaseModel,
15+
PullRequestModel,
16+
)
17+
18+
19+
@pytest.fixture
20+
def mock_pr_functionality(request):
21+
packit_yaml = "{'specfile_path': 'the-specfile.spec', 'jobs':" + str(request.param) + "}"
22+
flexmock(
23+
GithubProject,
24+
full_repo_name="packit/hello-world",
25+
get_file_content=lambda path, ref, headers: packit_yaml,
26+
get_files=lambda ref, filter_regex: ["the-specfile.spec"],
27+
get_web_url=lambda: "https://github.com/the-namespace/the-repo",
28+
get_pr=lambda pr_id: flexmock(head_commit="12345"),
29+
)
30+
flexmock(Github, get_repo=lambda full_name_or_id: None)
31+
32+
pr_model = (
33+
flexmock(PullRequestModel(pr_id=123))
34+
.should_receive("get_project_event_models")
35+
.and_return([flexmock(commit_sha="12345")])
36+
.mock()
37+
)
38+
project_event = (
39+
flexmock(ProjectEventModel(type=JobConfigTriggerType.pull_request, id=123456))
40+
.should_receive("get_project_event_object")
41+
.and_return(pr_model)
42+
.mock()
43+
.should_receive("set_packages_config")
44+
.mock()
45+
)
46+
flexmock(LocalProject, refresh_the_arguments=lambda: None)
47+
flexmock(ProjectEventModel).should_receive("get_or_create").and_return(
48+
project_event,
49+
)
50+
flexmock(ProjectEventModel).should_receive("get_by_id").with_args(
51+
123456,
52+
).and_return(project_event)
53+
flexmock(PullRequestModel).should_receive("get_or_create").with_args(
54+
pr_id=123,
55+
namespace="packit",
56+
repo_name="hello-world",
57+
project_url="https://github.com/packit/hello-world",
58+
).and_return(pr_model)
59+
flexmock(PullRequestModel).should_receive("get_by_id").with_args(123).and_return(
60+
pr_model,
61+
)
62+
63+
64+
@pytest.fixture
65+
def mock_push_functionality(request):
66+
packit_yaml = "{'specfile_path': 'the-specfile.spec', 'jobs':" + str(request.param) + "}"
67+
flexmock(
68+
GithubProject,
69+
full_repo_name="packit/hello-world",
70+
get_file_content=lambda path, ref, headers: packit_yaml,
71+
get_files=lambda ref, filter_regex: ["the-specfile.spec"],
72+
get_web_url=lambda: "https://github.com/the-namespace/the-repo",
73+
get_pr=lambda pr_id: flexmock(head_commit="12345"),
74+
)
75+
flexmock(Github, get_repo=lambda full_name_or_id: None)
76+
77+
branch_model = (
78+
flexmock(GitBranchModel(name="main"))
79+
.should_receive("get_project_event_models")
80+
.and_return([flexmock(commit_sha="12345")])
81+
.mock()
82+
)
83+
project_event = (
84+
flexmock(ProjectEventModel(type=JobConfigTriggerType.commit, id=123456))
85+
.should_receive("get_project_event_object")
86+
.and_return(branch_model)
87+
.mock()
88+
.should_receive("set_packages_config")
89+
.mock()
90+
)
91+
92+
flexmock(LocalProject, refresh_the_arguments=lambda: None)
93+
flexmock(ProjectEventModel).should_receive("get_or_create").and_return(
94+
project_event,
95+
)
96+
flexmock(ProjectEventModel).should_receive("get_by_id").with_args(
97+
123456,
98+
).and_return(project_event)
99+
flexmock(GitBranchModel).should_receive("get_or_create").with_args(
100+
branch_name="main",
101+
namespace="packit",
102+
repo_name="hello-world",
103+
project_url="https://github.com/packit/hello-world",
104+
).and_return(branch_model)
105+
flexmock(GitBranchModel).should_receive("get_by_id").with_args(123).and_return(
106+
branch_model,
107+
)
108+
109+
110+
@pytest.fixture
111+
def mock_release_functionality(request):
112+
packit_yaml = "{'specfile_path': 'the-specfile.spec', 'jobs':" + str(request.param) + "}"
113+
flexmock(
114+
GithubProject,
115+
full_repo_name="packit/hello-world",
116+
get_file_content=lambda path, ref, headers: packit_yaml,
117+
get_files=lambda ref, filter_regex: ["the-specfile.spec"],
118+
get_web_url=lambda: "https://github.com/the-namespace/the-repo",
119+
get_pr=lambda pr_id: flexmock(head_commit="12345"),
120+
)
121+
flexmock(Github, get_repo=lambda full_name_or_id: None)
122+
123+
release_model = (
124+
flexmock(ProjectReleaseModel(tag_name="0.1.0"))
125+
.should_receive("get_project_event_models")
126+
.and_return([flexmock(commit_sha="12345")])
127+
.mock()
128+
)
129+
project_event = (
130+
flexmock(ProjectEventModel(type=JobConfigTriggerType.release, id=123456))
131+
.should_receive("get_project_event_object")
132+
.and_return(release_model)
133+
.mock()
134+
.should_receive("set_packages_config")
135+
.mock()
136+
)
137+
flexmock(LocalProject, refresh_the_arguments=lambda: None)
138+
flexmock(ProjectEventModel).should_receive("get_by_id").with_args(
139+
123456,
140+
).and_return(project_event)
141+
flexmock(ProjectEventModel).should_receive("get_or_create").and_return(
142+
project_event,
143+
)
144+
flexmock(ProjectReleaseModel).should_receive("get_by_id").with_args(123).and_return(
145+
release_model,
146+
)
147+
flexmock(ProjectReleaseModel).should_receive("get_or_create").with_args(
148+
tag_name="0.1.0",
149+
namespace="packit",
150+
repo_name="hello-world",
151+
project_url="https://github.com/packit/hello-world",
152+
commit_hash="0e5d8b51fd5dfa460605e1497d22a76d65c6d7fd",
153+
).and_return(release_model)

0 commit comments

Comments
 (0)