Merge ~cjwatson/launchpad:charm-recipe-build-mailer into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 8ea9a832e05af2c37fe7648176ddaa575583ace2
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:charm-recipe-build-mailer
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:charm-recipe-request-builds-job
Diff against target: 241 lines (+164/-1)
5 files modified
lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt (+9/-0)
lib/lp/charms/mail/__init__.py (+0/-0)
lib/lp/charms/mail/charmrecipebuild.py (+85/-0)
lib/lp/charms/model/charmrecipebuild.py (+3/-1)
lib/lp/charms/tests/test_charmrecipebuild.py (+67/-0)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Cristian Gonzalez (community) Approve
Review via email: mp+403731@code.launchpad.net

Commit message

Add charm recipe build notifications

To post a comment you must log in.
Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Looks good!

review: Approve
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt b/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt
0new file mode 1006440new file mode 100644
index 0000000..e7630e3
--- /dev/null
+++ b/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt
@@ -0,0 +1,9 @@
1 * Charm Recipe: %(recipe_name)s
2 * Project: %(project_name)s
3 * Distroseries: %(distroseries)s
4 * Architecture: %(architecturetag)s
5 * State: %(build_state)s
6 * Duration: %(build_duration)s
7 * Build Log: %(log_url)s
8 * Upload Log: %(upload_log_url)s
9 * Builder: %(builder_url)s
diff --git a/lib/lp/charms/mail/__init__.py b/lib/lp/charms/mail/__init__.py
0new file mode 10064410new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/charms/mail/__init__.py
diff --git a/lib/lp/charms/mail/charmrecipebuild.py b/lib/lp/charms/mail/charmrecipebuild.py
1new file mode 10064411new file mode 100644
index 0000000..4486809
--- /dev/null
+++ b/lib/lp/charms/mail/charmrecipebuild.py
@@ -0,0 +1,85 @@
1# Copyright 2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import absolute_import, print_function, unicode_literals
5
6__metaclass__ = type
7__all__ = [
8 "CharmRecipeBuildMailer",
9 ]
10
11from lp.app.browser.tales import DurationFormatterAPI
12from lp.services.config import config
13from lp.services.mail.basemailer import (
14 BaseMailer,
15 RecipientReason,
16 )
17from lp.services.webapp import canonical_url
18
19
20class CharmRecipeBuildMailer(BaseMailer):
21
22 app = "charms"
23
24 @classmethod
25 def forStatus(cls, build):
26 """Create a mailer for notifying about charm recipe build status.
27
28 :param build: The relevant build.
29 """
30 requester = build.requester
31 recipients = {requester: RecipientReason.forBuildRequester(requester)}
32 return cls(
33 "[Charm recipe build #%(build_id)d] %(build_title)s",
34 "charmrecipebuild-notification.txt", recipients,
35 config.canonical.noreply_from_address, "charm-recipe-build-status",
36 build)
37
38 def __init__(self, subject, template_name, recipients, from_address,
39 notification_type, build):
40 super(CharmRecipeBuildMailer, self).__init__(
41 subject, template_name, recipients, from_address,
42 notification_type=notification_type)
43 self.build = build
44
45 def _getHeaders(self, email, recipient):
46 """See `BaseMailer`."""
47 headers = super(CharmRecipeBuildMailer, self)._getHeaders(
48 email, recipient)
49 headers["X-Launchpad-Build-State"] = self.build.status.name
50 return headers
51
52 def _getTemplateParams(self, email, recipient):
53 """See `BaseMailer`."""
54 build = self.build
55 params = super(CharmRecipeBuildMailer, self)._getTemplateParams(
56 email, recipient)
57 params.update({
58 "architecturetag": build.distro_arch_series.architecturetag,
59 "build_duration": "",
60 "build_id": build.id,
61 "build_state": build.status.title,
62 "build_title": build.title,
63 "build_url": canonical_url(build),
64 "builder_url": "",
65 "distroseries": build.distro_series,
66 "log_url": "",
67 "project_name": build.recipe.project.name,
68 "recipe_name": build.recipe.name,
69 "upload_log_url": "",
70 })
71 if build.duration is not None:
72 duration_formatter = DurationFormatterAPI(build.duration)
73 params["build_duration"] = duration_formatter.approximateduration()
74 if build.log is not None:
75 params["log_url"] = build.log_url
76 if build.upload_log is not None:
77 params["upload_log_url"] = build.upload_log_url
78 if build.builder is not None:
79 params["builder_url"] = canonical_url(build.builder)
80 return params
81
82 def _getFooter(self, email, recipient, params):
83 """See `BaseMailer`."""
84 return ("%(build_url)s\n"
85 "%(reason)s\n" % params)
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index 214f084..24d7ff7 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -44,6 +44,7 @@ from lp.charms.interfaces.charmrecipebuild import (
44 ICharmRecipeBuild,44 ICharmRecipeBuild,
45 ICharmRecipeBuildSet,45 ICharmRecipeBuildSet,
46 )46 )
47from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
47from lp.registry.interfaces.pocket import PackagePublishingPocket48from lp.registry.interfaces.pocket import PackagePublishingPocket
48from lp.registry.interfaces.series import SeriesStatus49from lp.registry.interfaces.series import SeriesStatus
49from lp.registry.model.person import Person50from lp.registry.model.person import Person
@@ -389,7 +390,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
389 return390 return
390 if self.status == BuildStatus.FULLYBUILT:391 if self.status == BuildStatus.FULLYBUILT:
391 return392 return
392 # XXX cjwatson 2021-05-28: Send email notifications.393 mailer = CharmRecipeBuildMailer.forStatus(self)
394 mailer.sendAll()
393395
394396
395@implementer(ICharmRecipeBuildSet)397@implementer(ICharmRecipeBuildSet)
diff --git a/lib/lp/charms/tests/test_charmrecipebuild.py b/lib/lp/charms/tests/test_charmrecipebuild.py
index a6b5557..7258d3a 100644
--- a/lib/lp/charms/tests/test_charmrecipebuild.py
+++ b/lib/lp/charms/tests/test_charmrecipebuild.py
@@ -13,6 +13,7 @@ from datetime import (
13 )13 )
1414
15import pytz15import pytz
16import six
16from testtools.matchers import Equals17from testtools.matchers import Equals
17from zope.component import getUtility18from zope.component import getUtility
18from zope.security.proxy import removeSecurityProxy19from zope.security.proxy import removeSecurityProxy
@@ -22,6 +23,7 @@ from lp.app.errors import NotFoundError
22from lp.buildmaster.enums import BuildStatus23from lp.buildmaster.enums import BuildStatus
23from lp.buildmaster.interfaces.buildqueue import IBuildQueue24from lp.buildmaster.interfaces.buildqueue import IBuildQueue
24from lp.buildmaster.interfaces.packagebuild import IPackageBuild25from lp.buildmaster.interfaces.packagebuild import IPackageBuild
26from lp.buildmaster.interfaces.processor import IProcessorSet
25from lp.charms.interfaces.charmrecipe import (27from lp.charms.interfaces.charmrecipe import (
26 CHARM_RECIPE_ALLOW_CREATE,28 CHARM_RECIPE_ALLOW_CREATE,
27 CHARM_RECIPE_PRIVATE_FEATURE_FLAG,29 CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
@@ -35,6 +37,7 @@ from lp.registry.enums import (
35 TeamMembershipPolicy,37 TeamMembershipPolicy,
36 )38 )
37from lp.registry.interfaces.series import SeriesStatus39from lp.registry.interfaces.series import SeriesStatus
40from lp.services.config import config
38from lp.services.features.testing import FeatureFixture41from lp.services.features.testing import FeatureFixture
39from lp.services.propertycache import clear_property_cache42from lp.services.propertycache import clear_property_cache
40from lp.testing import (43from lp.testing import (
@@ -43,9 +46,23 @@ from lp.testing import (
43 TestCaseWithFactory,46 TestCaseWithFactory,
44 )47 )
45from lp.testing.layers import LaunchpadZopelessLayer48from lp.testing.layers import LaunchpadZopelessLayer
49from lp.testing.mail_helpers import pop_notifications
46from lp.testing.matchers import HasQueryCount50from lp.testing.matchers import HasQueryCount
4751
4852
53expected_body = """\
54 * Charm Recipe: charm-1
55 * Project: charm-project
56 * Distroseries: distro unstable
57 * Architecture: i386
58 * State: Failed to build
59 * Duration: 10 minutes
60 * Build Log: %s
61 * Upload Log: %s
62 * Builder: http://launchpad.test/builders/bob
63"""
64
65
49class TestCharmRecipeBuild(TestCaseWithFactory):66class TestCharmRecipeBuild(TestCaseWithFactory):
5067
51 layer = LaunchpadZopelessLayer68 layer = LaunchpadZopelessLayer
@@ -239,6 +256,56 @@ class TestCharmRecipeBuild(TestCaseWithFactory):
239 BuildStatus.BUILDING, slave_status={"revision_id": "dummy"})256 BuildStatus.BUILDING, slave_status={"revision_id": "dummy"})
240 self.assertEqual("dummy", self.build.revision_id)257 self.assertEqual("dummy", self.build.revision_id)
241258
259 def test_notify_fullybuilt(self):
260 # notify does not send mail when a recipe build completes normally.
261 build = self.factory.makeCharmRecipeBuild(
262 status=BuildStatus.FULLYBUILT)
263 build.notify()
264 self.assertEqual(0, len(pop_notifications()))
265
266 def test_notify_packagefail(self):
267 # notify sends mail when a recipe build fails.
268 person = self.factory.makePerson(name="person")
269 project = self.factory.makeProduct(name="charm-project")
270 distribution = self.factory.makeDistribution(name="distro")
271 distroseries = self.factory.makeDistroSeries(
272 distribution=distribution, name="unstable")
273 processor = getUtility(IProcessorSet).getByName("386")
274 das = self.factory.makeDistroArchSeries(
275 distroseries=distroseries, architecturetag="i386",
276 processor=processor)
277 build = self.factory.makeCharmRecipeBuild(
278 name="charm-1", requester=person, owner=person, project=project,
279 distro_arch_series=das, status=BuildStatus.FAILEDTOBUILD,
280 builder=self.factory.makeBuilder(name="bob"),
281 duration=timedelta(minutes=10))
282 build.setLog(self.factory.makeLibraryFileAlias())
283 build.notify()
284 [notification] = pop_notifications()
285 self.assertEqual(
286 config.canonical.noreply_from_address, notification["From"])
287 self.assertEqual(
288 "Person <%s>" % person.preferredemail.email, notification["To"])
289 subject = notification["Subject"].replace("\n ", " ")
290 self.assertEqual(
291 "[Charm recipe build #%d] i386 build of "
292 "/~person/charm-project/+charm/charm-1" % build.id, subject)
293 self.assertEqual(
294 "Requester", notification["X-Launchpad-Message-Rationale"])
295 self.assertEqual(person.name, notification["X-Launchpad-Message-For"])
296 self.assertEqual(
297 "charm-recipe-build-status",
298 notification["X-Launchpad-Notification-Type"])
299 self.assertEqual(
300 "FAILEDTOBUILD", notification["X-Launchpad-Build-State"])
301 body, footer = six.ensure_text(
302 notification.get_payload(decode=True)).split("\n-- \n")
303 self.assertEqual(expected_body % (build.log_url, ""), body)
304 self.assertEqual(
305 "http://launchpad.test/~person/charm-project/+charm/charm-1/"
306 "+build/%d\n"
307 "You are the requester of the build.\n" % build.id, footer)
308
242 def addFakeBuildLog(self, build):309 def addFakeBuildLog(self, build):
243 build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))310 build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))
244311

Subscribers

People subscribed via source and target branches

to status/vote changes: