launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24241
[Merge] ~ilasc/launchpad:webhook-livefs into launchpad:master
Ioana Lasc has proposed merging ~ilasc/launchpad:webhook-livefs into launchpad:master with ~cjwatson/launchpad:fix-webhook-visibility-check as a prerequisite.
Commit message:
Add Webhook for LiveFS builds
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/377875
Added webhooks for LiveFS builds.
While open for review, I did notice a test failure at the end of my day in lp.code.scripts.tests.test_request_daily_builds.TestRequestDailyBuilds.test_request_daily_builds after running full test suite with the message: "ERROR - team-name-100107/snap-name-100108/arch-100021: permission denied for relation account"
Will investigate in the morning - wanted to make sure I get the tidying up and the MP open today.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/launchpad:webhook-livefs into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 06da0d4..ff7c406 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2596,6 +2596,7 @@ public.branch = SELECT
public.distribution = SELECT
public.gitrepository = SELECT
public.job = SELECT, UPDATE
+public.livefs = SELECT
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ociprojectseries = SELECT
diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py
index e4533b7..8fe724e 100644
--- a/lib/lp/services/webhooks/interfaces.py
+++ b/lib/lp/services/webhooks/interfaces.py
@@ -75,6 +75,7 @@ from lp.services.webservice.apihelpers import (
WEBHOOK_EVENT_TYPES = {
"bzr:push:0.1": "Bazaar push",
"git:push:0.1": "Git push",
+ "livefs:build:0.1": "Live filesystem build",
"merge-proposal:0.1": "Merge proposal",
"snap:build:0.1": "Snap build",
}
diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py
index 0c38e8e..440ce26 100644
--- a/lib/lp/services/webhooks/model.py
+++ b/lib/lp/services/webhooks/model.py
@@ -104,6 +104,9 @@ class Webhook(StormBase):
snap_id = Int(name='snap')
snap = Reference(snap_id, 'Snap.id')
+ livefs_id = Int(name='livefs')
+ livefs = Reference(livefs_id, 'LiveFS.id')
+
registrant_id = Int(name='registrant', allow_none=False)
registrant = Reference(registrant_id, 'Person.id')
date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -123,6 +126,8 @@ class Webhook(StormBase):
return self.branch
elif self.snap is not None:
return self.snap
+ elif self.livefs is not None:
+ return self.livefs
else:
raise AssertionError("No target.")
@@ -177,6 +182,8 @@ class WebhookSet:
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
from lp.snappy.interfaces.snap import ISnap
+ from lp.soyuz.interfaces.livefs import ILiveFS
+
hook = Webhook()
if IGitRepository.providedBy(target):
hook.git_repository = target
@@ -184,6 +191,8 @@ class WebhookSet:
hook.branch = target
elif ISnap.providedBy(target):
hook.snap = target
+ elif ILiveFS.providedBy(target):
+ hook.livefs = target
else:
raise AssertionError("Unsupported target: %r" % (target,))
hook.registrant = registrant
@@ -208,12 +217,16 @@ class WebhookSet:
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
from lp.snappy.interfaces.snap import ISnap
+ from lp.soyuz.interfaces.livefs import ILiveFS
+
if IGitRepository.providedBy(target):
target_filter = Webhook.git_repository == target
elif IBranch.providedBy(target):
target_filter = Webhook.branch == target
elif ISnap.providedBy(target):
target_filter = Webhook.snap == target
+ elif ILiveFS.providedBy(target):
+ target_filter = Webhook.livefs == target
else:
raise AssertionError("Unsupported target: %r" % (target,))
return IStore(Webhook).find(Webhook, target_filter).order_by(
diff --git a/lib/lp/services/webhooks/tests/test_browser.py b/lib/lp/services/webhooks/tests/test_browser.py
index 22a4be7..9abccc0 100644
--- a/lib/lp/services/webhooks/tests/test_browser.py
+++ b/lib/lp/services/webhooks/tests/test_browser.py
@@ -18,6 +18,7 @@ import transaction
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.publisher import canonical_url
from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
+from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
from lp.testing import (
login_person,
record_two_runs,
@@ -55,12 +56,11 @@ batch_nav_tag = soupmatchers.Tag(
class GitRepositoryTestHelpers:
-
event_type = "git:push:0.1"
expected_event_types = [
("git:push:0.1", "Git push"),
("merge-proposal:0.1", "Merge proposal"),
- ]
+ ]
def makeTarget(self):
return self.factory.makeGitRepository()
@@ -70,12 +70,11 @@ class GitRepositoryTestHelpers:
class BranchTestHelpers:
-
event_type = "bzr:push:0.1"
expected_event_types = [
("bzr:push:0.1", "Bazaar push"),
("merge-proposal:0.1", "Merge proposal"),
- ]
+ ]
def makeTarget(self):
return self.factory.makeBranch()
@@ -85,11 +84,10 @@ class BranchTestHelpers:
class SnapTestHelpers:
-
event_type = "snap:build:0.1"
expected_event_types = [
("snap:build:0.1", "Snap build"),
- ]
+ ]
def setUp(self):
super(SnapTestHelpers, self).setUp()
@@ -101,7 +99,7 @@ class SnapTestHelpers:
def makeTarget(self):
self.useFixture(FeatureFixture({
'webhooks.new.enabled': 'true',
- }))
+ }))
owner = self.factory.makePerson()
return self.factory.makeSnap(registrant=owner, owner=owner)
@@ -109,6 +107,25 @@ class SnapTestHelpers:
return [obj]
+class LiveFSTestHelpers:
+ event_type = "livefs:build:0.1"
+ expected_event_types = [
+ ("livefs:build:0.1", "Live filesystem build"),
+ ]
+
+ def setUp(self):
+ super(LiveFSTestHelpers, self).setUp()
+
+ def makeTarget(self):
+ self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true',
+ LIVEFS_FEATURE_FLAG: "on"}))
+ owner = self.factory.makePerson()
+ return self.factory.makeLiveFS(registrant=owner, owner=owner)
+
+ def getTraversalStack(self, obj):
+ return [obj]
+
+
class WebhookTargetViewTestHelpers:
def setUp(self):
@@ -122,13 +139,12 @@ class WebhookTargetViewTestHelpers:
view = create_view(self.target, name, principal=self.owner, **kwargs)
# To test the breadcrumbs we need a correct traversal stack.
view.request.traversed_objects = (
- self.getTraversalStack(self.target) + [view])
+ self.getTraversalStack(self.target) + [view])
view.initialize()
return view
class TestWebhooksViewBase(WebhookTargetViewTestHelpers):
-
layer = DatabaseFunctionalLayer
def makeHooksAndMatchers(self, count):
@@ -189,24 +205,25 @@ class TestWebhooksViewBase(WebhookTargetViewTestHelpers):
class TestWebhooksViewGitRepository(
TestWebhooksViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhooksViewBranch(
TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhooksViewSnap(
TestWebhooksViewBase, SnapTestHelpers, TestCaseWithFactory):
+ layer = LaunchpadFunctionalLayer
+
+class TestWebhooksViewLiveFS(
+ TestWebhooksViewBase, LiveFSTestHelpers, TestCaseWithFactory):
layer = LaunchpadFunctionalLayer
class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
-
layer = DatabaseFunctionalLayer
def test_rendering(self):
@@ -287,19 +304,21 @@ class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
class TestWebhookAddViewGitRepository(
TestWebhookAddViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookAddViewBranch(
TestWebhookAddViewBase, BranchTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookAddViewSnap(
TestWebhookAddViewBase, SnapTestHelpers, TestCaseWithFactory):
+ layer = LaunchpadFunctionalLayer
+
+class TestWebhookAddViewLiveFS(
+ TestWebhookAddViewBase, LiveFSTestHelpers, TestCaseWithFactory):
layer = LaunchpadFunctionalLayer
@@ -318,13 +337,12 @@ class WebhookViewTestHelpers:
view = create_view(self.webhook, name, principal=self.owner, **kwargs)
# To test the breadcrumbs we need a correct traversal stack.
view.request.traversed_objects = (
- self.getTraversalStack(self.target) + [self.webhook, view])
+ self.getTraversalStack(self.target) + [self.webhook, view])
view.initialize()
return view
class TestWebhookViewBase(WebhookViewTestHelpers):
-
layer = DatabaseFunctionalLayer
def test_rendering(self):
@@ -389,24 +407,25 @@ class TestWebhookViewBase(WebhookViewTestHelpers):
class TestWebhookViewGitRepository(
TestWebhookViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookViewBranch(
TestWebhookViewBase, BranchTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookViewSnap(
TestWebhookViewBase, SnapTestHelpers, TestCaseWithFactory):
+ pass
+
+class TestWebhookViewLiveFS(
+ TestWebhookViewBase, LiveFSTestHelpers, TestCaseWithFactory):
pass
class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
-
layer = DatabaseFunctionalLayer
def test_rendering(self):
@@ -441,17 +460,19 @@ class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
class TestWebhookDeleteViewGitRepository(
TestWebhookDeleteViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookDeleteViewBranch(
TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
-
pass
class TestWebhookDeleteViewSnap(
TestWebhookDeleteViewBase, SnapTestHelpers, TestCaseWithFactory):
+ pass
+
+class TestWebhookDeleteViewLiveFS(
+ TestWebhookDeleteViewBase, LiveFSTestHelpers, TestCaseWithFactory):
pass
diff --git a/lib/lp/services/webhooks/tests/test_job.py b/lib/lp/services/webhooks/tests/test_job.py
index 260698c..4f6f43e 100644
--- a/lib/lp/services/webhooks/tests/test_job.py
+++ b/lib/lp/services/webhooks/tests/test_job.py
@@ -60,6 +60,7 @@ from lp.services.webhooks.model import (
WebhookJobType,
)
from lp.services.webhooks.testing import LogsScheduledWebhooks
+from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
from lp.testing import (
login_person,
TestCaseWithFactory,
@@ -339,6 +340,16 @@ class TestWebhookDeliveryJob(TestCaseWithFactory):
"<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, snap),
repr(job))
+ def test_livefs__repr__(self):
+ # `WebhookDeliveryJob` objects for livefs have an informative __repr__.
+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on"}):
+ livefs = self.factory.makeLiveFS()
+ hook = self.factory.makeWebhook(target=livefs)
+ job = WebhookDeliveryJob.create(hook, 'test', payload={'foo': 'bar'})
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, livefs),
+ repr(job))
+
def test_short_lease_and_timeout(self):
# Webhook jobs have a request timeout of 30 seconds, a celery
# timeout of 45 seconds, and a lease of 60 seconds, to give
diff --git a/lib/lp/services/webhooks/tests/test_model.py b/lib/lp/services/webhooks/tests/test_model.py
index 41189e3..1e98a68 100644
--- a/lib/lp/services/webhooks/tests/test_model.py
+++ b/lib/lp/services/webhooks/tests/test_model.py
@@ -15,6 +15,7 @@ from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
from lp.registry.enums import BranchSharingPolicy
from lp.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.snapshot import notify_modified
from lp.services.webhooks.interfaces import IWebhookSet
@@ -22,6 +23,7 @@ from lp.services.webhooks.model import (
WebhookJob,
WebhookSet,
)
+from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
from lp.testing import (
admin_logged_in,
anonymous_logged_in,
@@ -395,3 +397,16 @@ class TestWebhookSetSnap(TestWebhookSetBase, TestCaseWithFactory):
if owner is None:
owner = self.factory.makePerson()
return self.factory.makeSnap(registrant=owner, owner=owner, **kwargs)
+
+
+class TestWebhookSetLiveFS(TestWebhookSetBase, TestCaseWithFactory):
+
+ event_type = 'livefs:build:0.1'
+
+ def makeTarget(self, owner=None, **kwargs):
+ if owner is None:
+ owner = self.factory.makePerson()
+
+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on"}):
+ return self.factory.makeLiveFS(registrant=owner,
+ owner=owner, **kwargs)
diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py
index cdf92b5..f8abf63 100644
--- a/lib/lp/services/webhooks/tests/test_webservice.py
+++ b/lib/lp/services/webhooks/tests/test_webservice.py
@@ -23,6 +23,7 @@ from zope.security.proxy import removeSecurityProxy
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import OAuthPermission
+from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
from lp.testing import (
api_url,
person_logged_in,
@@ -320,7 +321,8 @@ class TestWebhookTargetBase:
for entry in representation['entries']])
def test_newWebhook_secret(self):
- self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
+ self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true',
+ LIVEFS_FEATURE_FLAG: "on"}))
response = self.webservice.named_post(
self.target_url, 'newWebhook',
delivery_url='http://example.com/ep',
@@ -379,3 +381,13 @@ class TestWebhookTargetSnap(TestWebhookTargetBase, TestCaseWithFactory):
def makeTarget(self):
owner = self.factory.makePerson()
return self.factory.makeSnap(registrant=owner, owner=owner)
+
+
+class TestWebhookTargetLiveFS(TestWebhookTargetBase, TestCaseWithFactory):
+
+ event_type = 'livefs:build:0.1'
+
+ def makeTarget(self):
+ owner = self.factory.makePerson()
+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on"}):
+ return self.factory.makeLiveFS(registrant=owner, owner=owner)
diff --git a/lib/lp/soyuz/browser/livefs.py b/lib/lp/soyuz/browser/livefs.py
index 5f1bfb9..e2659ca 100644
--- a/lib/lp/soyuz/browser/livefs.py
+++ b/lib/lp/soyuz/browser/livefs.py
@@ -55,6 +55,7 @@ from lp.services.webapp.breadcrumb import (
Breadcrumb,
NameBreadcrumb,
)
+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.soyuz.browser.build import get_build_by_id_str
from lp.soyuz.interfaces.livefs import (
ILiveFS,
@@ -66,7 +67,7 @@ from lp.soyuz.interfaces.livefs import (
from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
-class LiveFSNavigation(Navigation):
+class LiveFSNavigation(WebhookTargetNavigationMixin, Navigation):
usedfor = ILiveFS
@stepthrough('+build')
diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
index 0e35ccb..da011cc 100644
--- a/lib/lp/soyuz/configure.zcml
+++ b/lib/lp/soyuz/configure.zcml
@@ -1022,6 +1022,14 @@
permission="launchpad.Admin"
interface=".interfaces.livefsbuild.ILiveFSBuildAdmin"/>
</class>
+ <subscriber
+ for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild
+ lazr.lifecycle.interfaces.IObjectCreatedEvent"
+ handler="lp.soyuz.subscribers.livefsbuild.livefs_build_created" />
+ <subscriber
+ for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild
+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
+ handler="lp.soyuz.subscribers.livefsbuild.livefs_build_status_changed" />
<!-- LiveFSBuildSet -->
<securedutility
diff --git a/lib/lp/soyuz/interfaces/livefs.py b/lib/lp/soyuz/interfaces/livefs.py
index 140f874..9038ed9 100644
--- a/lib/lp/soyuz/interfaces/livefs.py
+++ b/lib/lp/soyuz/interfaces/livefs.py
@@ -69,6 +69,7 @@ from lp.services.fields import (
PersonChoice,
PublicPersonChoice,
)
+from lp.services.webhooks.interfaces import IWebhookTarget
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
@@ -213,7 +214,7 @@ class ILiveFSView(IPrivacy):
value_type=Reference(schema=Interface), readonly=True)))
-class ILiveFSEdit(Interface):
+class ILiveFSEdit(IWebhookTarget):
"""`ILiveFS` methods that require launchpad.Edit permission."""
@export_destructor_operation()
diff --git a/lib/lp/soyuz/model/livefs.py b/lib/lp/soyuz/model/livefs.py
index b79c1ca..c273f0f 100644
--- a/lib/lp/soyuz/model/livefs.py
+++ b/lib/lp/soyuz/model/livefs.py
@@ -9,6 +9,7 @@ __all__ = [
from datetime import timedelta
import math
+from lazr.lifecycle.event import ObjectCreatedEvent
import pytz
from storm.locals import (
Bool,
@@ -24,6 +25,7 @@ from storm.locals import (
Unicode,
)
from zope.component import getUtility
+from zope.event import notify
from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
@@ -57,6 +59,7 @@ from lp.services.database.stormexpr import (
)
from lp.services.features import getFeatureFlag
from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webhooks.model import WebhookTargetMixin
from lp.soyuz.interfaces.archive import ArchiveDisabled
from lp.soyuz.interfaces.livefs import (
CannotDeleteLiveFS,
@@ -88,7 +91,7 @@ def livefs_modified(livefs, event):
@implementer(ILiveFS, IHasOwner)
-class LiveFS(Storm):
+class LiveFS(Storm, WebhookTargetMixin):
"""See `ILiveFS`."""
__storm_table__ = 'LiveFS'
@@ -139,6 +142,10 @@ class LiveFS(Storm):
self.keep_binary_files_days = keep_binary_files_days
@property
+ def valid_webhook_event_types(self):
+ return ["livefs:build:0.1"]
+
+ @property
def private(self):
"""See `IPrivacy`."""
# A LiveFS has no privacy support of its own, but it is private if
@@ -191,6 +198,7 @@ class LiveFS(Storm):
unique_key=unique_key, metadata_override=metadata_override,
version=version)
build.queueBuild()
+ notify(ObjectCreatedEvent(build, user=requester))
return build
def _getBuilds(self, filter_term, order_by):
diff --git a/lib/lp/soyuz/model/livefsbuild.py b/lib/lp/soyuz/model/livefsbuild.py
index 310ab5c..2bf500e 100644
--- a/lib/lp/soyuz/model/livefsbuild.py
+++ b/lib/lp/soyuz/model/livefsbuild.py
@@ -50,6 +50,7 @@ from lp.services.librarian.model import (
LibraryFileAlias,
LibraryFileContent,
)
+from lp.services.webapp.snapshot import notify_modified
from lp.soyuz.interfaces.component import IComponentSet
from lp.soyuz.interfaces.livefs import (
LIVEFS_FEATURE_FLAG,
@@ -315,6 +316,20 @@ class LiveFSBuild(PackageBuildMixin, Storm):
"""See `IPackageBuild`."""
return not self.getFiles().is_empty()
+ def updateStatus(self, status, builder=None, slave_status=None,
+ date_started=None, date_finished=None,
+ force_invalid_transition=False):
+ """See `IBuildFarmJob`."""
+
+ edited_fields = set()
+ with notify_modified(self, edited_fields) as previous_obj:
+ super(LiveFSBuild, self).updateStatus(
+ status, builder=builder, slave_status=slave_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 not config.builddmaster.send_build_notification:
diff --git a/lib/lp/soyuz/subscribers/__init__.py b/lib/lp/soyuz/subscribers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/soyuz/subscribers/__init__.py
diff --git a/lib/lp/soyuz/subscribers/livefsbuild.py b/lib/lp/soyuz/subscribers/livefsbuild.py
new file mode 100644
index 0000000..c74df6b
--- /dev/null
+++ b/lib/lp/soyuz/subscribers/livefsbuild.py
@@ -0,0 +1,43 @@
+# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Event subscribers for livefs builds."""
+from __future__ import absolute_import, print_function, unicode_literals
+
+
+__metaclass__ = type
+
+
+from zope.component import getUtility
+
+from lp.services.features import getFeatureFlag
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.payload import compose_webhook_payload
+from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
+from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
+
+
+def _trigger_livefs_build_webhook(livefsbuild, action):
+ if getFeatureFlag(LIVEFS_FEATURE_FLAG):
+ payload = {
+ "livefs_build": canonical_url(livefsbuild, force_local_path=True),
+ "action": action,
+ }
+ payload.update(compose_webhook_payload(
+ ILiveFSBuild, livefsbuild,
+ ["livefs", "status"]))
+ getUtility(IWebhookSet).trigger(
+ livefsbuild.livefs, "livefs:build:0.1", payload)
+
+
+def livefs_build_created(livefsbuild, event):
+ """Trigger events when a new livefs build is created."""
+ _trigger_livefs_build_webhook(livefsbuild, "created")
+
+
+def livefs_build_status_changed(livefsbuild, event):
+ """Trigger events when livefs package build statuses change."""
+ if event.edited_fields is not None:
+ if "status" in event.edited_fields:
+ _trigger_livefs_build_webhook(livefsbuild, "status-changed")
diff --git a/lib/lp/soyuz/tests/test_livefsbuild.py b/lib/lp/soyuz/tests/test_livefsbuild.py
index 987c6b1..06027f9 100644
--- a/lib/lp/soyuz/tests/test_livefsbuild.py
+++ b/lib/lp/soyuz/tests/test_livefsbuild.py
@@ -13,7 +13,14 @@ from datetime import (
)
from urllib2 import urlopen
+from fixtures import FakeLogger
import pytz
+from testtools.matchers import (
+ ContainsDict,
+ Equals,
+ MatchesDict,
+ MatchesStructure,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -31,6 +38,8 @@ from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.webapp.interfaces import OAuthPermission
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.soyuz.enums import ArchivePurpose
from lp.soyuz.interfaces.livefs import (
LIVEFS_FEATURE_FLAG,
@@ -48,6 +57,7 @@ from lp.testing import (
person_logged_in,
TestCaseWithFactory,
)
+from lp.testing.dbuser import dbuser
from lp.testing.layers import (
LaunchpadFunctionalLayer,
LaunchpadZopelessLayer,
@@ -162,6 +172,61 @@ class TestLiveFSBuild(TestCaseWithFactory):
self.assertEqual(BuildStatus.CANCELLED, self.build.status)
self.assertIsNone(self.build.buildqueue_record)
+ def test_updateStatus_triggers_webhooks(self):
+ # Updating the status of a SnapBuild triggers webhooks on the
+ # corresponding Snap.
+ logger = self.useFixture(FakeLogger())
+ hook = self.factory.makeWebhook(
+ target=self.build.livefs, event_types=["livefs:build:0.1"])
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ expected_payload = {
+ "livefs_build": Equals(
+ canonical_url(self.build, force_local_path=True)),
+ "action": Equals("status-changed"),
+ "livefs": Equals(
+ canonical_url(self.build.livefs, force_local_path=True)),
+ "status": Equals("Successfully built"),
+ }
+ self.assertThat(
+ logger.output, LogsScheduledWebhooks([
+ (hook, "livefs:build:0.1", MatchesDict(expected_payload))]))
+
+ delivery = hook.deliveries.one()
+ self.assertThat(
+ delivery, MatchesStructure(
+ event_type=Equals("livefs:build:0.1"),
+ payload=MatchesDict(expected_payload)))
+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (
+ hook.id, hook.target),
+ repr(delivery))
+
+ def test_updateStatus_no_change_does_not_trigger_webhooks(self):
+ # An updateStatus call that changes details such as the revision_id
+ # but that doesn't change the build's status attribute does not
+ # trigger webhooks.
+ logger = self.useFixture(FakeLogger())
+ hook = self.factory.makeWebhook(
+ target=self.build.livefs, event_types=["livefs:build:0.1"])
+ self.build.updateStatus(BuildStatus.BUILDING)
+ expected_logs = [
+ (hook, "livefs:build:0.1", ContainsDict({
+ "action": Equals("status-changed"),
+ "status": Equals("Currently building"),
+ }))]
+ self.assertEqual(1, hook.deliveries.count())
+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
+
+ self.build.updateStatus(BuildStatus.BUILDING)
+ expected_logs = [
+ (hook, "livefs:build:0.1", ContainsDict({
+ "action": Equals("status-changed"),
+ "status": Equals("Currently building"),
+ }))]
+ self.assertEqual(1, hook.deliveries.count())
+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
+
def test_cancel_in_progress(self):
# The cancel() method for a building build leaves it in the
# CANCELLING state.