Skip to content

Commit 0d1da39

Browse files
MDBF-XXXX: Prefer pull_request over push for same-commit tarball builds
Buildbot treats push and pull_request events independently. The scheduler can trigger tarball-docker for both event types, while push events are only considered when the upstream branch matches a configured pattern such as bb-* or st-*. When a pull request is opened from a branch in the upstream repository, the source branch may also match the push-event filter. In that case, two tarball builds can be scheduled for the same commit: one from the push event and one from the pull_request event. This patch cancels the tarball triggered by the push event and keeps only the pull_request build, since that is the one relevant for branch protection and GitHub Checks. Note that *-pkgtest builds cannot be run meaningfully from both a pull request and the corresponding upstream branch at the same time. Any pkgtest should therefore be completed before opening the pull request.
1 parent de010bf commit 0d1da39

2 files changed

Lines changed: 219 additions & 1 deletion

File tree

master-protected-branches/master.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from common_factories import getLastNFailedBuildsFactory, getQuickBuildFactory
1010
from locks import getLocks
1111
from master_common import base_master_config
1212
from utils import (
13+
CancelDuplicateBuildRequests,
1314
canStartBuild,
1415
createWorker,
1516
isJepsenBranch,
@@ -198,6 +199,7 @@ for w_name in ["aarch64-bbw"]:
198199

199200
## f_tarball - create source tarball
200201
f_tarball = util.BuildFactory()
202+
f_tarball.addStep(CancelDuplicateBuildRequests())
201203
f_tarball.addStep(
202204
steps.ShellCommand(command=["echo", " revision: ", util.Property("revision")])
203205
)

utils.py

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
from twisted.python import log
1111

1212
from buildbot.buildrequest import BuildRequest
13+
from buildbot.data.resultspec import Filter
1314
from buildbot.interfaces import IProperties
1415
from buildbot.master import BuildMaster
1516
from buildbot.plugins import steps, util, worker
1617
from buildbot.process.builder import Builder
1718
from buildbot.process.buildstep import BuildStep
18-
from buildbot.process.results import FAILURE
19+
from buildbot.process.results import FAILURE, SUCCESS
1920
from buildbot.process.workerforbuilder import AbstractWorkerForBuilder
2021
from buildbot.worker import AbstractWorker
2122
from constants import (
@@ -678,3 +679,218 @@ def mtrEnv(props: IProperties) -> dict:
678679
mtr_add_env[key] = value
679680
return mtr_add_env
680681
return MTR_ENV
682+
683+
684+
class CancelDuplicateBuildRequests(BuildStep):
685+
"""BuildStep to cancel duplicate buildrequests for the same commit on the same builder
686+
if the current build is for a pull request event. It checks for other pending buildrequests
687+
with the same builder and if they have a sourcestamp with the same revision as
688+
the current build, it cancels them. It also checks that the branch is cancelable to avoid
689+
canceling important branches like main, release or merge branches.
690+
"""
691+
692+
name = "cancel duplicate buildrequests"
693+
description = ["checking duplicate buildrequests"]
694+
descriptionDone = ["duplicate buildrequests checked"]
695+
696+
def __init__(self, dry_run=False, buildbot_base_url=None, **kwargs):
697+
super().__init__(**kwargs)
698+
self.dry_run = dry_run
699+
self.buildbot_base_url = (
700+
buildbot_base_url.rstrip("/") if buildbot_base_url else None
701+
)
702+
self._builder_name_cache = {}
703+
704+
def _buildrequest_url(self, brid):
705+
if not self.buildbot_base_url:
706+
return None
707+
return f"{self.buildbot_base_url}/#/buildrequests/{brid}"
708+
709+
@staticmethod
710+
def _fmt_ss(ss):
711+
return (
712+
f"branch={ss.get('branch')!r}, "
713+
f"repository={ss.get('repository')!r}, "
714+
f"revision={ss.get('revision')!r}, "
715+
f"codebase={ss.get('codebase', '')!r}"
716+
)
717+
718+
@staticmethod
719+
def _branch_is_cancelable(branch):
720+
"""Should not cancel pushes on main, release or merge branches"""
721+
if not branch:
722+
return False
723+
724+
branch_lc = branch.lower()
725+
return (
726+
len(branch) > 5 and "release" not in branch_lc and "merge" not in branch_lc
727+
)
728+
729+
@defer.inlineCallbacks
730+
def run(self):
731+
lines = []
732+
lines.append(f"Mode: {'DRY-RUN' if self.dry_run else 'ACTIVE'}")
733+
lines.append("")
734+
735+
event = self.getProperty("event", None)
736+
lines.append(f"event property: {event!r}")
737+
738+
if event is None:
739+
lines.append("No 'event' property found; nothing to do.")
740+
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
741+
return SUCCESS
742+
743+
if event != "pull_request":
744+
lines.append("Event is not 'pull_request'; nothing to do.")
745+
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
746+
return SUCCESS
747+
748+
current_buildid = self.build.buildid
749+
current_build = yield self.master.data.get(("builds", current_buildid))
750+
751+
current_buildrequestid = current_build["buildrequestid"]
752+
current_builderid = current_build["builderid"]
753+
754+
current_buildrequest = yield self.master.data.get(
755+
("buildrequests", current_buildrequestid)
756+
)
757+
current_buildsetid = current_buildrequest["buildsetid"]
758+
759+
current_buildset = yield self.master.data.get(("buildsets", current_buildsetid))
760+
current_sourcestamps = current_buildset.get("sourcestamps", [])
761+
762+
if not current_sourcestamps:
763+
lines.append("Current buildset has no sourcestamps; nothing to do.")
764+
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
765+
return SUCCESS
766+
767+
current_revisions = {
768+
ss.get("revision")
769+
for ss in current_sourcestamps
770+
if ss.get("revision") is not None
771+
}
772+
773+
if not current_revisions:
774+
lines.append(
775+
"Current buildset has no revision in sourcestamps; nothing to do."
776+
)
777+
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
778+
return SUCCESS
779+
780+
lines.append("Current:")
781+
lines.append(f" buildid={current_buildid}")
782+
lines.append(f" buildrequestid={current_buildrequestid}")
783+
lines.append(f" builderid={current_builderid}")
784+
lines.append(f" buildsetid={current_buildsetid}")
785+
lines.append(f" revisions={sorted(current_revisions)!r}")
786+
current_url = self._buildrequest_url(current_buildrequestid)
787+
if current_url:
788+
lines.append(f" url={current_url}")
789+
lines.append(" sourcestamps:")
790+
for i, ss in enumerate(current_sourcestamps, 1):
791+
lines.append(f" [{i}] {self._fmt_ss(ss)}")
792+
lines.append("")
793+
794+
filters = [
795+
Filter("complete", "eq", [False]),
796+
Filter("builderid", "eq", [current_builderid]),
797+
]
798+
799+
buildrequests = yield self.master.data.get(
800+
("buildrequests",),
801+
filters=filters,
802+
fields=[
803+
"buildrequestid",
804+
"buildsetid",
805+
"builderid",
806+
"claimed",
807+
"complete",
808+
],
809+
)
810+
811+
matches = []
812+
actions = []
813+
814+
for br in buildrequests:
815+
brid = br["buildrequestid"]
816+
817+
# skip self
818+
if brid == current_buildrequestid:
819+
continue
820+
821+
other_buildsetid = br["buildsetid"]
822+
other_buildset = yield self.master.data.get(("buildsets", other_buildsetid))
823+
other_sourcestamps = other_buildset.get("sourcestamps", [])
824+
825+
matched_revision = None
826+
matched_branch = None
827+
828+
for other_ss in other_sourcestamps:
829+
other_revision = other_ss.get("revision")
830+
other_branch = other_ss.get("branch")
831+
832+
if other_revision not in current_revisions:
833+
continue
834+
835+
if not self._branch_is_cancelable(other_branch):
836+
continue
837+
838+
matched_revision = other_revision
839+
matched_branch = other_branch
840+
break
841+
842+
if matched_revision is None:
843+
continue
844+
845+
info = {
846+
"buildrequestid": brid,
847+
"claimed": br.get("claimed"),
848+
"complete": br.get("complete"),
849+
"buildsetid": other_buildsetid,
850+
"revision": matched_revision,
851+
"branch": matched_branch,
852+
"url": self._buildrequest_url(brid),
853+
"sourcestamps": other_sourcestamps,
854+
}
855+
matches.append(info)
856+
857+
action_prefix = "[DRY-RUN] would cancel" if self.dry_run else "Canceled"
858+
msg = (
859+
f"{action_prefix} buildrequest {brid} "
860+
f"(claimed={br.get('claimed')}, "
861+
f"revision={matched_revision!r}, "
862+
f"branch={matched_branch!r})"
863+
)
864+
if info["url"]:
865+
msg += f" url={info['url']}"
866+
actions.append(msg)
867+
868+
if not self.dry_run:
869+
yield self.master.data.control(
870+
"cancel",
871+
{"reason": "Duplicate build for same commit on same builder"},
872+
("buildrequests", brid),
873+
)
874+
875+
lines.append(f"Matched duplicate buildrequests: {len(matches)}")
876+
lines.append("")
877+
878+
if matches:
879+
lines.append("Matches:")
880+
for m in matches:
881+
lines.append(f" - buildrequestid={m['buildrequestid']}")
882+
lines.append(f" claimed={m['claimed']}")
883+
lines.append(f" complete={m['complete']}")
884+
lines.append(f" buildsetid={m['buildsetid']}")
885+
lines.append(f" revision={m['revision']!r}")
886+
lines.append(f" branch={m['branch']!r}")
887+
if m["url"]:
888+
lines.append(f" url={m['url']}")
889+
lines.append("")
890+
lines.append("Actions:")
891+
lines.extend(f" {a}" for a in actions)
892+
else:
893+
lines.append("No matching cancelable buildrequests found on this builder.")
894+
895+
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
896+
return SUCCESS

0 commit comments

Comments
 (0)