launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31413
[Merge] ~jugmac00/launchpad:add-rock-recipe-build-notifications into launchpad:master
Jürgen Gmach has proposed merging ~jugmac00/launchpad:add-rock-recipe-build-notifications into launchpad:master with ~jugmac00/launchpad:implement-rockrecipe-requestbuild as a prerequisite.
Commit message:
Add rock recipe build notifications
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/472955
similar to https://git.launchpad.net/launchpad/commit/?id=8ea9a832e05af2c37fe7648176ddaa575583ace2
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:add-rock-recipe-build-notifications into launchpad:master.
diff --git a/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt b/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt
new file mode 100644
index 0000000..af69ceb
--- /dev/null
+++ b/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt
@@ -0,0 +1,9 @@
+ * Rock Recipe: %(recipe_name)s
+ * Project: %(project_name)s
+ * Distroseries: %(distroseries)s
+ * Architecture: %(architecturetag)s
+ * State: %(build_state)s
+ * Duration: %(build_duration)s
+ * Build Log: %(log_url)s
+ * Upload Log: %(upload_log_url)s
+ * Builder: %(builder_url)s
diff --git a/lib/lp/rocks/mail/__init__.py b/lib/lp/rocks/mail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/rocks/mail/__init__.py
diff --git a/lib/lp/rocks/mail/rockrecipebuild.py b/lib/lp/rocks/mail/rockrecipebuild.py
new file mode 100644
index 0000000..5146f98
--- /dev/null
+++ b/lib/lp/rocks/mail/rockrecipebuild.py
@@ -0,0 +1,92 @@
+# Copyright 2024 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+ "RockRecipeBuildMailer",
+]
+
+from lp.app.browser.tales import DurationFormatterAPI
+from lp.services.config import config
+from lp.services.mail.basemailer import BaseMailer, RecipientReason
+from lp.services.webapp import canonical_url
+
+
+class RockRecipeBuildMailer(BaseMailer):
+
+ app = "rocks"
+
+ @classmethod
+ def forStatus(cls, build):
+ """Create a mailer for notifying about rock recipe build status.
+
+ :param build: The relevant build.
+ """
+ requester = build.requester
+ recipients = {requester: RecipientReason.forBuildRequester(requester)}
+ return cls(
+ "[Rock recipe build #%(build_id)d] %(build_title)s",
+ "rockrecipebuild-notification.txt",
+ recipients,
+ config.canonical.noreply_from_address,
+ "rock-recipe-build-status",
+ build,
+ )
+
+ def __init__(
+ self,
+ subject,
+ template_name,
+ recipients,
+ from_address,
+ notification_type,
+ build,
+ ):
+ super().__init__(
+ subject,
+ template_name,
+ recipients,
+ from_address,
+ notification_type=notification_type,
+ )
+ self.build = build
+
+ def _getHeaders(self, email, recipient):
+ """See `BaseMailer`."""
+ headers = super()._getHeaders(email, recipient)
+ headers["X-Launchpad-Build-State"] = self.build.status.name
+ return headers
+
+ def _getTemplateParams(self, email, recipient):
+ """See `BaseMailer`."""
+ build = self.build
+ params = super()._getTemplateParams(email, recipient)
+ params.update(
+ {
+ "architecturetag": build.distro_arch_series.architecturetag,
+ "build_duration": "",
+ "build_id": build.id,
+ "build_state": build.status.title,
+ "build_title": build.title,
+ "build_url": canonical_url(build),
+ "builder_url": "",
+ "distroseries": build.distro_series,
+ "log_url": "",
+ "project_name": build.recipe.project.name,
+ "recipe_name": build.recipe.name,
+ "upload_log_url": "",
+ }
+ )
+ if build.duration is not None:
+ duration_formatter = DurationFormatterAPI(build.duration)
+ params["build_duration"] = duration_formatter.approximateduration()
+ if build.log is not None:
+ params["log_url"] = build.log_url
+ if build.upload_log is not None:
+ params["upload_log_url"] = build.upload_log_url
+ if build.builder is not None:
+ params["builder_url"] = canonical_url(build.builder)
+ return params
+
+ def _getFooter(self, email, recipient, params):
+ """See `BaseMailer`."""
+ return "%(build_url)s\n" "%(reason)s\n" % params
diff --git a/lib/lp/rocks/model/rockrecipebuild.py b/lib/lp/rocks/model/rockrecipebuild.py
index b5935b2..e6025e5 100644
--- a/lib/lp/rocks/model/rockrecipebuild.py
+++ b/lib/lp/rocks/model/rockrecipebuild.py
@@ -35,6 +35,7 @@ from lp.rocks.interfaces.rockrecipebuild import (
IRockRecipeBuild,
IRockRecipeBuildSet,
)
+from lp.rocks.mail.rockrecipebuild import RockRecipeBuildMailer
from lp.services.config import config
from lp.services.database.bulk import load_related
from lp.services.database.constants import DEFAULT
@@ -347,7 +348,8 @@ class RockRecipeBuild(PackageBuildMixin, StormBase):
return
if self.status == BuildStatus.FULLYBUILT:
return
- # XXX jugmac00 2024-09-03: Send email notifications.
+ mailer = RockRecipeBuildMailer.forStatus(self)
+ mailer.sendAll()
@implementer(IRockRecipeBuildSet)
diff --git a/lib/lp/rocks/tests/test_rockrecipebuild.py b/lib/lp/rocks/tests/test_rockrecipebuild.py
index 662c2aa..a9d6ffe 100644
--- a/lib/lp/rocks/tests/test_rockrecipebuild.py
+++ b/lib/lp/rocks/tests/test_rockrecipebuild.py
@@ -2,9 +2,9 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test rock package build features."""
-
from datetime import datetime, timedelta, timezone
+import six
from testtools.matchers import Equals
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -14,6 +14,7 @@ from lp.app.errors import NotFoundError
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueue
from lp.buildmaster.interfaces.packagebuild import IPackageBuild
+from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
from lp.registry.interfaces.series import SeriesStatus
from lp.rocks.interfaces.rockrecipe import (
@@ -24,6 +25,7 @@ from lp.rocks.interfaces.rockrecipebuild import (
IRockRecipeBuild,
IRockRecipeBuildSet,
)
+from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.propertycache import clear_property_cache
from lp.testing import (
@@ -32,8 +34,21 @@ from lp.testing import (
person_logged_in,
)
from lp.testing.layers import LaunchpadZopelessLayer
+from lp.testing.mail_helpers import pop_notifications
from lp.testing.matchers import HasQueryCount
+expected_body = """\
+ * Rock Recipe: rock-1
+ * Project: rock-project
+ * Distroseries: distro unstable
+ * Architecture: i386
+ * State: Failed to build
+ * Duration: 10 minutes
+ * Build Log: %s
+ * Upload Log: %s
+ * Builder: http://launchpad.test/builders/bob
+"""
+
class TestRockRecipeBuild(TestCaseWithFactory):
@@ -257,6 +272,73 @@ class TestRockRecipeBuild(TestCaseWithFactory):
)
self.assertEqual("dummy", self.build.revision_id)
+ def test_notify_fullybuilt(self):
+ # notify does not send mail when a recipe build completes normally.
+ build = self.factory.makeRockRecipeBuild(status=BuildStatus.FULLYBUILT)
+ build.notify()
+ self.assertEqual(0, len(pop_notifications()))
+
+ def test_notify_packagefail(self):
+ # notify sends mail when a recipe build fails.
+ person = self.factory.makePerson(name="person")
+ project = self.factory.makeProduct(name="rock-project")
+ distribution = self.factory.makeDistribution(name="distro")
+ distroseries = self.factory.makeDistroSeries(
+ distribution=distribution, name="unstable"
+ )
+ processor = getUtility(IProcessorSet).getByName("386")
+ das = self.factory.makeDistroArchSeries(
+ distroseries=distroseries,
+ architecturetag="i386",
+ processor=processor,
+ )
+ build = self.factory.makeRockRecipeBuild(
+ name="rock-1",
+ requester=person,
+ owner=person,
+ project=project,
+ distro_arch_series=das,
+ status=BuildStatus.FAILEDTOBUILD,
+ builder=self.factory.makeBuilder(name="bob"),
+ duration=timedelta(minutes=10),
+ )
+ build.setLog(self.factory.makeLibraryFileAlias())
+ build.notify()
+ [notification] = pop_notifications()
+ self.assertEqual(
+ config.canonical.noreply_from_address, notification["From"]
+ )
+ self.assertEqual(
+ "Person <%s>" % person.preferredemail.email, notification["To"]
+ )
+ subject = notification["Subject"].replace("\n ", " ")
+ self.assertEqual(
+ "[Rock recipe build #%d] i386 build of "
+ "/~person/rock-project/+rock/rock-1" % build.id,
+ subject,
+ )
+ self.assertEqual(
+ "Requester", notification["X-Launchpad-Message-Rationale"]
+ )
+ self.assertEqual(person.name, notification["X-Launchpad-Message-For"])
+ self.assertEqual(
+ "rock-recipe-build-status",
+ notification["X-Launchpad-Notification-Type"],
+ )
+ self.assertEqual(
+ "FAILEDTOBUILD", notification["X-Launchpad-Build-State"]
+ )
+ body, footer = six.ensure_text(
+ notification.get_payload(decode=True)
+ ).split("\n-- \n")
+ self.assertEqual(expected_body % (build.log_url, ""), body)
+ self.assertEqual(
+ "http://launchpad.test/~person/rock-project/+rock/rock-1/"
+ "+build/%d\n"
+ "You are the requester of the build.\n" % build.id,
+ footer,
+ )
+
def addFakeBuildLog(self, build):
build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))