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
1diff --git a/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt b/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt
2new file mode 100644
3index 0000000..e7630e3
4--- /dev/null
5+++ b/lib/lp/charms/emailtemplates/charmrecipebuild-notification.txt
6@@ -0,0 +1,9 @@
7+ * Charm Recipe: %(recipe_name)s
8+ * Project: %(project_name)s
9+ * Distroseries: %(distroseries)s
10+ * Architecture: %(architecturetag)s
11+ * State: %(build_state)s
12+ * Duration: %(build_duration)s
13+ * Build Log: %(log_url)s
14+ * Upload Log: %(upload_log_url)s
15+ * Builder: %(builder_url)s
16diff --git a/lib/lp/charms/mail/__init__.py b/lib/lp/charms/mail/__init__.py
17new file mode 100644
18index 0000000..e69de29
19--- /dev/null
20+++ b/lib/lp/charms/mail/__init__.py
21diff --git a/lib/lp/charms/mail/charmrecipebuild.py b/lib/lp/charms/mail/charmrecipebuild.py
22new file mode 100644
23index 0000000..4486809
24--- /dev/null
25+++ b/lib/lp/charms/mail/charmrecipebuild.py
26@@ -0,0 +1,85 @@
27+# Copyright 2021 Canonical Ltd. This software is licensed under the
28+# GNU Affero General Public License version 3 (see the file LICENSE).
29+
30+from __future__ import absolute_import, print_function, unicode_literals
31+
32+__metaclass__ = type
33+__all__ = [
34+ "CharmRecipeBuildMailer",
35+ ]
36+
37+from lp.app.browser.tales import DurationFormatterAPI
38+from lp.services.config import config
39+from lp.services.mail.basemailer import (
40+ BaseMailer,
41+ RecipientReason,
42+ )
43+from lp.services.webapp import canonical_url
44+
45+
46+class CharmRecipeBuildMailer(BaseMailer):
47+
48+ app = "charms"
49+
50+ @classmethod
51+ def forStatus(cls, build):
52+ """Create a mailer for notifying about charm recipe build status.
53+
54+ :param build: The relevant build.
55+ """
56+ requester = build.requester
57+ recipients = {requester: RecipientReason.forBuildRequester(requester)}
58+ return cls(
59+ "[Charm recipe build #%(build_id)d] %(build_title)s",
60+ "charmrecipebuild-notification.txt", recipients,
61+ config.canonical.noreply_from_address, "charm-recipe-build-status",
62+ build)
63+
64+ def __init__(self, subject, template_name, recipients, from_address,
65+ notification_type, build):
66+ super(CharmRecipeBuildMailer, self).__init__(
67+ subject, template_name, recipients, from_address,
68+ notification_type=notification_type)
69+ self.build = build
70+
71+ def _getHeaders(self, email, recipient):
72+ """See `BaseMailer`."""
73+ headers = super(CharmRecipeBuildMailer, self)._getHeaders(
74+ email, recipient)
75+ headers["X-Launchpad-Build-State"] = self.build.status.name
76+ return headers
77+
78+ def _getTemplateParams(self, email, recipient):
79+ """See `BaseMailer`."""
80+ build = self.build
81+ params = super(CharmRecipeBuildMailer, self)._getTemplateParams(
82+ email, recipient)
83+ params.update({
84+ "architecturetag": build.distro_arch_series.architecturetag,
85+ "build_duration": "",
86+ "build_id": build.id,
87+ "build_state": build.status.title,
88+ "build_title": build.title,
89+ "build_url": canonical_url(build),
90+ "builder_url": "",
91+ "distroseries": build.distro_series,
92+ "log_url": "",
93+ "project_name": build.recipe.project.name,
94+ "recipe_name": build.recipe.name,
95+ "upload_log_url": "",
96+ })
97+ if build.duration is not None:
98+ duration_formatter = DurationFormatterAPI(build.duration)
99+ params["build_duration"] = duration_formatter.approximateduration()
100+ if build.log is not None:
101+ params["log_url"] = build.log_url
102+ if build.upload_log is not None:
103+ params["upload_log_url"] = build.upload_log_url
104+ if build.builder is not None:
105+ params["builder_url"] = canonical_url(build.builder)
106+ return params
107+
108+ def _getFooter(self, email, recipient, params):
109+ """See `BaseMailer`."""
110+ return ("%(build_url)s\n"
111+ "%(reason)s\n" % params)
112diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
113index 214f084..24d7ff7 100644
114--- a/lib/lp/charms/model/charmrecipebuild.py
115+++ b/lib/lp/charms/model/charmrecipebuild.py
116@@ -44,6 +44,7 @@ from lp.charms.interfaces.charmrecipebuild import (
117 ICharmRecipeBuild,
118 ICharmRecipeBuildSet,
119 )
120+from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
121 from lp.registry.interfaces.pocket import PackagePublishingPocket
122 from lp.registry.interfaces.series import SeriesStatus
123 from lp.registry.model.person import Person
124@@ -389,7 +390,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
125 return
126 if self.status == BuildStatus.FULLYBUILT:
127 return
128- # XXX cjwatson 2021-05-28: Send email notifications.
129+ mailer = CharmRecipeBuildMailer.forStatus(self)
130+ mailer.sendAll()
131
132
133 @implementer(ICharmRecipeBuildSet)
134diff --git a/lib/lp/charms/tests/test_charmrecipebuild.py b/lib/lp/charms/tests/test_charmrecipebuild.py
135index a6b5557..7258d3a 100644
136--- a/lib/lp/charms/tests/test_charmrecipebuild.py
137+++ b/lib/lp/charms/tests/test_charmrecipebuild.py
138@@ -13,6 +13,7 @@ from datetime import (
139 )
140
141 import pytz
142+import six
143 from testtools.matchers import Equals
144 from zope.component import getUtility
145 from zope.security.proxy import removeSecurityProxy
146@@ -22,6 +23,7 @@ from lp.app.errors import NotFoundError
147 from lp.buildmaster.enums import BuildStatus
148 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
149 from lp.buildmaster.interfaces.packagebuild import IPackageBuild
150+from lp.buildmaster.interfaces.processor import IProcessorSet
151 from lp.charms.interfaces.charmrecipe import (
152 CHARM_RECIPE_ALLOW_CREATE,
153 CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
154@@ -35,6 +37,7 @@ from lp.registry.enums import (
155 TeamMembershipPolicy,
156 )
157 from lp.registry.interfaces.series import SeriesStatus
158+from lp.services.config import config
159 from lp.services.features.testing import FeatureFixture
160 from lp.services.propertycache import clear_property_cache
161 from lp.testing import (
162@@ -43,9 +46,23 @@ from lp.testing import (
163 TestCaseWithFactory,
164 )
165 from lp.testing.layers import LaunchpadZopelessLayer
166+from lp.testing.mail_helpers import pop_notifications
167 from lp.testing.matchers import HasQueryCount
168
169
170+expected_body = """\
171+ * Charm Recipe: charm-1
172+ * Project: charm-project
173+ * Distroseries: distro unstable
174+ * Architecture: i386
175+ * State: Failed to build
176+ * Duration: 10 minutes
177+ * Build Log: %s
178+ * Upload Log: %s
179+ * Builder: http://launchpad.test/builders/bob
180+"""
181+
182+
183 class TestCharmRecipeBuild(TestCaseWithFactory):
184
185 layer = LaunchpadZopelessLayer
186@@ -239,6 +256,56 @@ class TestCharmRecipeBuild(TestCaseWithFactory):
187 BuildStatus.BUILDING, slave_status={"revision_id": "dummy"})
188 self.assertEqual("dummy", self.build.revision_id)
189
190+ def test_notify_fullybuilt(self):
191+ # notify does not send mail when a recipe build completes normally.
192+ build = self.factory.makeCharmRecipeBuild(
193+ status=BuildStatus.FULLYBUILT)
194+ build.notify()
195+ self.assertEqual(0, len(pop_notifications()))
196+
197+ def test_notify_packagefail(self):
198+ # notify sends mail when a recipe build fails.
199+ person = self.factory.makePerson(name="person")
200+ project = self.factory.makeProduct(name="charm-project")
201+ distribution = self.factory.makeDistribution(name="distro")
202+ distroseries = self.factory.makeDistroSeries(
203+ distribution=distribution, name="unstable")
204+ processor = getUtility(IProcessorSet).getByName("386")
205+ das = self.factory.makeDistroArchSeries(
206+ distroseries=distroseries, architecturetag="i386",
207+ processor=processor)
208+ build = self.factory.makeCharmRecipeBuild(
209+ name="charm-1", requester=person, owner=person, project=project,
210+ distro_arch_series=das, status=BuildStatus.FAILEDTOBUILD,
211+ builder=self.factory.makeBuilder(name="bob"),
212+ duration=timedelta(minutes=10))
213+ build.setLog(self.factory.makeLibraryFileAlias())
214+ build.notify()
215+ [notification] = pop_notifications()
216+ self.assertEqual(
217+ config.canonical.noreply_from_address, notification["From"])
218+ self.assertEqual(
219+ "Person <%s>" % person.preferredemail.email, notification["To"])
220+ subject = notification["Subject"].replace("\n ", " ")
221+ self.assertEqual(
222+ "[Charm recipe build #%d] i386 build of "
223+ "/~person/charm-project/+charm/charm-1" % build.id, subject)
224+ self.assertEqual(
225+ "Requester", notification["X-Launchpad-Message-Rationale"])
226+ self.assertEqual(person.name, notification["X-Launchpad-Message-For"])
227+ self.assertEqual(
228+ "charm-recipe-build-status",
229+ notification["X-Launchpad-Notification-Type"])
230+ self.assertEqual(
231+ "FAILEDTOBUILD", notification["X-Launchpad-Build-State"])
232+ body, footer = six.ensure_text(
233+ notification.get_payload(decode=True)).split("\n-- \n")
234+ self.assertEqual(expected_body % (build.log_url, ""), body)
235+ self.assertEqual(
236+ "http://launchpad.test/~person/charm-project/+charm/charm-1/"
237+ "+build/%d\n"
238+ "You are the requester of the build.\n" % build.id, footer)
239+
240 def addFakeBuildLog(self, build):
241 build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))
242

Subscribers

People subscribed via source and target branches

to status/vote changes: