# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Add notifications to tasks via Taskcluster's notify service. See https://docs.taskcluster.net/docs/reference/core/notify/usage for more information. """ from voluptuous import ALLOW_EXTRA, Any, Exclusive, Optional, Required from taskgraph.transforms.base import TransformSequence from taskgraph.util.schema import Schema, optionally_keyed_by, resolve_keyed_by _status_type = Any( "on-completed", "on-defined", "on-exception", "on-failed", "on-pending", "on-resolved", "on-running", ) _recipients = [ { Required("type"): "email", Required("address"): optionally_keyed_by("project", "level", str), Optional("status-type"): _status_type, }, { Required("type"): "matrix-room", Required("room-id"): str, Optional("status-type"): _status_type, }, { Required("type"): "pulse", Required("routing-key"): str, Optional("status-type"): _status_type, }, { Required("type"): "slack-channel", Required("channel-id"): str, Optional("status-type"): _status_type, }, ] _route_keys = { "email": "address", "matrix-room": "room-id", "pulse": "routing-key", "slack-channel": "channel-id", } """Map each type to its primary key that will be used in the route.""" NOTIFY_SCHEMA = Schema( { Exclusive("notify", "config"): { Required("recipients"): [Any(*_recipients)], Optional("content"): { Optional("email"): { Optional("subject"): str, Optional("content"): str, Optional("link"): { Required("text"): str, Required("href"): str, }, }, Optional("matrix"): { Optional("body"): str, Optional("formatted-body"): str, Optional("format"): str, Optional("msg-type"): str, }, Optional("slack"): { Optional("text"): str, Optional("blocks"): list, Optional("attachments"): list, }, }, }, # Continue supporting the legacy schema for backwards compat. Exclusive("notifications", "config"): { Required("emails"): optionally_keyed_by("project", "level", [str]), Required("subject"): str, Optional("message"): str, Optional("status-types"): [_status_type], }, }, extra=ALLOW_EXTRA, ) """Notify schema.""" transforms = TransformSequence() transforms.add_validate(NOTIFY_SCHEMA) def _convert_legacy(config, legacy, label): """Convert the legacy format to the new one.""" notify = { "recipients": [], "content": {"email": {"subject": legacy["subject"]}}, } resolve_keyed_by( legacy, "emails", label, **{ "level": config.params["level"], "project": config.params["project"], }, ) status_types = legacy.get("status-types", ["on-completed"]) for email in legacy["emails"]: for status_type in status_types: notify["recipients"].append( {"type": "email", "address": email, "status-type": status_type} ) notify["content"]["email"]["content"] = legacy.get("message", legacy["subject"]) return notify def _convert_content(content): """Convert the notify content to Taskcluster's format. The Taskcluster notification format is described here: https://docs.taskcluster.net/docs/reference/core/notify/usage """ tc = {} if "email" in content: tc["email"] = content.pop("email") for key, obj in content.items(): for name in obj.keys(): tc_name = "".join(part.capitalize() for part in name.split("-")) tc[f"{key}{tc_name}"] = obj[name] return tc @transforms.add def add_notifications(config, tasks): for task in tasks: label = "{}-{}".format(config.kind, task["name"]) if "notifications" in task: notify = _convert_legacy(config, task.pop("notifications"), label) else: notify = task.pop("notify", None) if not notify: yield task continue format_kwargs = dict( task=task, config=config.__dict__, ) def substitute(ctx): """Recursively find all strings in a simple nested dict (no lists), and format them in-place using `format_kwargs`.""" for key, val in ctx.items(): if isinstance(val, str): ctx[key] = val.format(**format_kwargs) elif isinstance(val, dict): ctx[key] = substitute(val) return ctx task.setdefault("routes", []) for recipient in notify["recipients"]: type = recipient["type"] recipient.setdefault("status-type", "on-completed") substitute(recipient) if type == "email": resolve_keyed_by( recipient, "address", label, **{ "level": config.params["level"], "project": config.params["project"], }, ) task["routes"].append( f"notify.{type}.{recipient[_route_keys[type]]}.{recipient['status-type']}" ) if "content" in notify: task.setdefault("extra", {}).update( {"notify": _convert_content(substitute(notify["content"]))} ) yield task