launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32911
[Merge] ~vaishnavi-asawale/launchpad:deb-build-webhook into launchpad:master
Vaishnavi Asawale has proposed merging ~vaishnavi-asawale/launchpad:deb-build-webhook into launchpad:master.
Commit message:
WIP: Add Launchpad Webhooks for deb builds
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~vaishnavi-asawale/launchpad/+git/launchpad/+merge/491578
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~vaishnavi-asawale/launchpad:deb-build-webhook into launchpad:master.
diff --git a/lib/lp/code/browser/sourcepackagerecipe.py b/lib/lp/code/browser/sourcepackagerecipe.py
index cd5d311..bae8cae 100644
--- a/lib/lp/code/browser/sourcepackagerecipe.py
+++ b/lib/lp/code/browser/sourcepackagerecipe.py
@@ -7,6 +7,7 @@ __all__ = [
"SourcePackageRecipeAddView",
"SourcePackageRecipeContextMenu",
"SourcePackageRecipeEditView",
+ "SourcePackageRecipeNavigation"
"SourcePackageRecipeNavigationMenu",
"SourcePackageRecipeRequestBuildsView",
"SourcePackageRecipeRequestDailyBuildView",
@@ -78,25 +79,49 @@ from lp.code.interfaces.sourcepackagerecipe import (
IRecipeBranchSource,
ISourcePackageRecipe,
ISourcePackageRecipeSource,
+ SOURCEPACKAGE_RECIPE_WEBHOOKS_FEATURE_FLAG
)
from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries
+from lp.code.interfaces.sourcepackagerecipebuild import ISourcePackageRecipeBuildSource
from lp.registry.interfaces.series import SeriesStatus
from lp.services.fields import PersonChoice
+from lp.services.features import getFeatureFlag
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
ContextMenu,
LaunchpadView,
Link,
+ Navigation,
NavigationMenu,
canonical_url,
enabled_with_permission,
structured,
)
from lp.services.webapp.authorization import check_permission
+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.services.webapp.breadcrumb import Breadcrumb, NameBreadcrumb
+from lp.soyuz.browser.build import get_build_by_id_str
from lp.soyuz.interfaces.archive import ArchiveDisabled
from lp.soyuz.model.archive import validate_ppa
+class SourcePackageRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
+ usedfor = ISourcePackageRecipe
+
+ @stepthrough("+build-request")
+ def traverse_build_request(self, name):
+ try:
+ job_id = int(name)
+ except ValueError:
+ return None
+ return self.context.getBuildRequest(job_id)
+
+ @stepthrough("+build")
+ def traverse_build(self, name):
+ build = get_build_by_id_str(ISourcePackageRecipeBuildSource, name)
+ if build is None or build.recipe != self.context:
+ return None
+ return build
+
class SourcePackageRecipeBreadcrumb(NameBreadcrumb):
@property
@@ -118,7 +143,7 @@ class SourcePackageRecipeNavigationMenu(NavigationMenu):
facet = "branches"
- links = ("edit", "delete")
+ links = ("edit", "delete", "webhooks")
@enabled_with_permission("launchpad.Edit")
def edit(self):
@@ -128,6 +153,13 @@ class SourcePackageRecipeNavigationMenu(NavigationMenu):
def delete(self):
return Link("+delete", "Delete recipe", icon="trash-icon")
+ @enabled_with_permission("launchpad.Edit")
+ def webhooks(self):
+ return Link(
+ "+webhooks", "Manage webhooks", icon="edit",
+ enabled=(
+ bool(getFeatureFlag(SOURCEPACKAGE_RECIPE_WEBHOOKS_FEATURE_FLAG)) and
+ bool(getFeatureFlag("webhooks.new.enabled"))))
class SourcePackageRecipeContextMenu(ContextMenu):
"""Context menu for sourcepackage recipes."""
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 95a84f9..7bc71b4 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -1307,6 +1307,14 @@
<subscriber
for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe zope.lifecycleevent.interfaces.IObjectModifiedEvent"
handler="lp.code.model.sourcepackagerecipe.recipe_modified"/>
+ <subscriber
+ for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild
+ lazr.lifecycle.interfaces.IObjectCreatedEvent"
+ handler="lp.code.subscribers.sourcepackagerecipe.sourcepackagerecipe_build_created" />
+<subscriber
+ for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild
+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
+ handler="lp.code.subscribers.sourcepackagerecipe.sourcepackagerecipe_build_modified" />
<!-- LPCIConfiguration -->
<class class="lp.code.model.lpci.LPCIConfiguration">
diff --git a/lib/lp/code/interfaces/sourcepackagerecipe.py b/lib/lp/code/interfaces/sourcepackagerecipe.py
index e4f7d52..a5c0301 100644
--- a/lib/lp/code/interfaces/sourcepackagerecipe.py
+++ b/lib/lp/code/interfaces/sourcepackagerecipe.py
@@ -12,6 +12,7 @@ __all__ = [
"MINIMAL_RECIPE_TEXT_BZR",
"MINIMAL_RECIPE_TEXT_GIT",
"RecipeBranchType",
+ "SOURCEPACKAGE_RECIPE_WEBHOOKS_FEATURE_FLAG",
]
@@ -44,8 +45,11 @@ from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.role import IHasOwner
from lp.services.fields import Description, PersonChoice, PublicPersonChoice
+from lp.services.webhooks.interfaces import IWebhookTarget
from lp.soyuz.interfaces.archive import IArchive
+SOURCEPACKAGE_RECIPE_WEBHOOKS_FEATURE_FLAG = "sourcepackage.recipe.webhooks.enabled"
+
MINIMAL_RECIPE_TEXT_BZR = dedent(
"""\
# bzr-builder format 0.3 deb-version {debupstream}-0~{revno}
@@ -320,7 +324,7 @@ class ISourcePackageRecipeEditableAttributes(IHasOwner):
)
-class ISourcePackageRecipeEdit(Interface):
+class ISourcePackageRecipeEdit(IWebhookTarget):
"""ISourcePackageRecipe methods that require launchpad.Edit permission."""
@mutator_for(ISourcePackageRecipeView["recipe_text"])
diff --git a/lib/lp/code/model/sourcepackagerecipe.py b/lib/lp/code/model/sourcepackagerecipe.py
index d30e1ec..33c7d92 100644
--- a/lib/lp/code/model/sourcepackagerecipe.py
+++ b/lib/lp/code/model/sourcepackagerecipe.py
@@ -49,6 +49,8 @@ from lp.services.database.interfaces import IPrimaryStore, IStore
from lp.services.database.stormbase import StormBase
from lp.services.database.stormexpr import Greatest, NullsLast
from lp.services.propertycache import cachedproperty, get_property_cache
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.model import WebhookTargetMixin
from lp.soyuz.model.archive import Archive
@@ -82,7 +84,7 @@ class _SourcePackageRecipeDistroSeries(StormBase):
@implementer(ISourcePackageRecipe)
@provider(ISourcePackageRecipeSource)
@delegate_to(ISourcePackageRecipeData, context="_recipe_data")
-class SourcePackageRecipe(StormBase):
+class SourcePackageRecipe(StormBase, WebhookTargetMixin):
"""See `ISourcePackageRecipe` and `ISourcePackageRecipeSource`."""
__storm_table__ = "SourcePackageRecipe"
@@ -150,6 +152,10 @@ class SourcePackageRecipe(StormBase):
assert self.base_git_repository is not None
return self.base_git_repository
+ @property
+ def valid_webhook_event_types(self):
+ return ["sourcepackage-recipe:build:0.1"]
+
@staticmethod
def preLoadDataForSourcePackageRecipes(sourcepackagerecipes):
# Load the referencing SourcePackageRecipeData.
@@ -285,6 +291,7 @@ class SourcePackageRecipe(StormBase):
)
builds.set(recipe_id=None)
store.remove(self._recipe_data)
+ getUtility(IWebhookSet).delete(self.webhooks)
store.remove(self)
def containsUnbuildableSeries(self, archive):
diff --git a/lib/lp/code/model/sourcepackagerecipebuild.py b/lib/lp/code/model/sourcepackagerecipebuild.py
index e3043d0..773d748 100644
--- a/lib/lp/code/model/sourcepackagerecipebuild.py
+++ b/lib/lp/code/model/sourcepackagerecipebuild.py
@@ -354,6 +354,31 @@ class SourcePackageRecipeBuild(
def verifySuccessfulUpload(self) -> bool:
return self.source_package_release is not None
+ def updateStatus(
+ self,
+ status,
+ builder=None,
+ worker_status=None,
+ date_started=None,
+ date_finished=None,
+ force_invalid_transition=False,
+ ):
+ """See `IBuildFarmJob`."""
+ edited_fields = set()
+ with notify_modified(
+ self, edited_fields, snapshot_names=("status",)
+ ) as previous_obj:
+ super().updateStatus(
+ status,
+ builder=builder,
+ worker_status=worker_status,
+ date_started=date_started,
+ date_finished=date_finished,
+ force_invalid_transition=force_invalid_transition,
+ )
+ if self.status != previous_obj.status:
+ edited_fields.add("status")
+
def notify(self, extra_info=None):
"""See `IPackageBuild`."""
# If our recipe has been deleted, any notification will fail.
diff --git a/lib/lp/code/subscribers/sourcepackagerecipebuild.py b/lib/lp/code/subscribers/sourcepackagerecipebuild.py
new file mode 100644
index 0000000..9029ea7
--- /dev/null
+++ b/lib/lp/code/subscribers/sourcepackagerecipebuild.py
@@ -0,0 +1,42 @@
+# Copyright 2025 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Event subscribers for Source Package Recipe builds."""
+
+from lazr.lifecycle.interfaces import IObjectCreatedEvent, IObjectModifiedEvent
+from zope.component import getUtility
+
+from lp.code.interfaces.sourcepackagerecipebuild import ISourcePackageRecipeBuild
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.payload import compose_webhook_payload
+
+
+def _trigger_sourcepackagerecipe_build_webhook(build: ISourcePackageRecipe, action: str) -> None:
+ """Trigger a webhook for a source package recipe build event."""
+ payload = {
+ "sourcepackagerecipe_build": canonical_url(build, force_local_path=True),
+ "action": action,
+ }
+ payload.update(
+ compose_webhook_payload(
+ ISourcePackageRecipeBuild,
+ build,
+ ["recipe", "build_request", "status"],
+ )
+ )
+ getUtility(IWebhookSet).trigger(
+ build.recipe, "sourcepackage-recipe:build:0.1", payload
+ )
+
+
+def sourcepackagerecipe_build_created(build: ISourcePackageRecipe, event: IObjectCreatedEvent) -> None:
+ """Trigger events when a new source package recipe build is created."""
+ _trigger_sourcepackagerecipe_build_webhook(build, "created")
+
+
+def sourcepackagerecipe_build_modified(build: ISourcePackageRecipe, event: IObjectModifiedEvent) -> None:
+ """Trigger events when a source package recipe build is modified."""
+ if event.edited_fields is not None:
+ if "status" in event.edited_fields:
+ _trigger_sourcepackagerecipe_build_webhook(build, "status-changed")
diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py
index 1515d2f..edffad0 100644
--- a/lib/lp/services/webhooks/interfaces.py
+++ b/lib/lp/services/webhooks/interfaces.py
@@ -67,6 +67,7 @@ WEBHOOK_EVENT_TYPES = {
"oci-recipe:build:0.1": "OCI recipe build",
"snap:build:0.1": "Snap build",
"craft-recipe:build:0.1": "Craft recipe build",
+ "sourcepackage-recipe:build:0.1": "Source package recipe build",
}
diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py
index 397c818..9351a1c 100644
--- a/lib/lp/services/webhooks/model.py
+++ b/lib/lp/services/webhooks/model.py
@@ -117,6 +117,9 @@ class Webhook(StormBase):
source_package_name_id, "SourcePackageName.id"
)
+ sourcepackage_recipe_id = Int(name="sourcepackage_recipe")
+ sourcepackage_recipe = Reference(sourcepackage_recipe_id, "SourcePackageRecipe.id")
+
registrant_id = Int(name="registrant", allow_none=False)
registrant = Reference(registrant_id, "Person.id")
date_created = DateTime(tzinfo=timezone.utc, allow_none=False)
@@ -233,6 +236,7 @@ class WebhookSet:
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
from lp.crafts.interfaces.craftrecipe import ICraftRecipe
+ from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.oci.interfaces.ocirecipe import IOCIRecipe
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
@@ -257,6 +261,8 @@ class WebhookSet:
hook.charm_recipe = target
elif ICraftRecipe.providedBy(target):
hook.craft_recipe = target
+ elif ISourcePackageRecipe.providedBy(target):
+ hook.sourcepackage_recipe = target
elif IProduct.providedBy(target):
hook.project = target
elif IDistribution.providedBy(target):
@@ -291,6 +297,7 @@ class WebhookSet:
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
from lp.crafts.interfaces.craftrecipe import ICraftRecipe
+ from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.oci.interfaces.ocirecipe import IOCIRecipe
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
@@ -314,6 +321,8 @@ class WebhookSet:
target_filter = Webhook.charm_recipe == target
elif ICraftRecipe.providedBy(target):
target_filter = Webhook.craft_recipe == target
+ elif ISourcePackageRecipe.providedBy(target):
+ target_filter = Webhook.sourcepackage_recipe == target
elif IProduct.providedBy(target):
target_filter = Webhook.project == target
elif IDistribution.providedBy(target):