2016-04-21 19:18:41 +02:00
|
|
|
# Webhooks for teamcity integration
|
2017-11-16 00:43:10 +01:00
|
|
|
import logging
|
|
|
|
|
2016-04-21 19:18:41 +02:00
|
|
|
from django.db.models import Q
|
2016-09-12 17:30:42 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2016-06-05 23:57:48 +02:00
|
|
|
|
2022-04-14 23:50:10 +02:00
|
|
|
from zerver.actions.message_send import (
|
2020-06-11 00:54:34 +02:00
|
|
|
check_send_private_message,
|
|
|
|
send_rate_limited_pm_notification_to_bot_owner,
|
|
|
|
)
|
2022-04-14 23:50:10 +02:00
|
|
|
from zerver.decorator import webhook_view
|
2023-08-12 09:34:31 +02:00
|
|
|
from zerver.lib.request import RequestNotes
|
2019-02-02 23:53:55 +01:00
|
|
|
from zerver.lib.response import json_success
|
2020-01-14 22:06:24 +01:00
|
|
|
from zerver.lib.send_email import FromAddress
|
2023-09-27 19:01:31 +02:00
|
|
|
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
2023-08-12 09:34:31 +02:00
|
|
|
from zerver.lib.validator import WildValue, check_string
|
2018-03-17 18:26:27 +01:00
|
|
|
from zerver.lib.webhooks.common import check_send_webhook_message
|
2017-11-16 00:43:10 +01:00
|
|
|
from zerver.models import Realm, UserProfile
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2018-08-01 21:03:30 +02:00
|
|
|
MISCONFIGURED_PAYLOAD_TYPE_ERROR_MESSAGE = """
|
|
|
|
Hi there! Your bot {bot_name} just received a TeamCity payload in a
|
|
|
|
format that Zulip doesn't recognize. This usually indicates a
|
|
|
|
configuration issue in your TeamCity webhook settings. Please make sure
|
|
|
|
that you set the **Payload Format** option to **Legacy Webhook (JSON)**
|
|
|
|
in your TeamCity webhook configuration. Contact {support_email} if you
|
|
|
|
need further help!
|
|
|
|
"""
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:23 +02:00
|
|
|
def guess_zulip_user_from_teamcity(teamcity_username: str, realm: Realm) -> UserProfile | None:
|
2016-04-21 19:18:41 +02:00
|
|
|
try:
|
|
|
|
# Try to find a matching user in Zulip
|
|
|
|
# We search a user's full name, short name,
|
|
|
|
# and beginning of email address
|
|
|
|
user = UserProfile.objects.filter(
|
2021-02-12 08:19:30 +01:00
|
|
|
Q(full_name__iexact=teamcity_username) | Q(email__istartswith=teamcity_username),
|
2017-01-24 07:06:13 +01:00
|
|
|
is_active=True,
|
2021-02-12 08:19:30 +01:00
|
|
|
realm=realm,
|
|
|
|
).order_by("id")[0]
|
2016-04-21 19:18:41 +02:00
|
|
|
return user
|
|
|
|
except IndexError:
|
|
|
|
return None
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:23 +02:00
|
|
|
def get_teamcity_property_value(property_list: WildValue, name: str) -> str | None:
|
2016-04-21 19:18:41 +02:00
|
|
|
for property in property_list:
|
2022-06-18 16:36:56 +02:00
|
|
|
if property["name"].tame(check_string) == name:
|
|
|
|
return property["value"].tame(check_string)
|
2016-04-21 19:18:41 +02:00
|
|
|
return None
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-12-17 05:13:07 +01:00
|
|
|
@webhook_view("TeamCity")
|
2023-08-12 09:34:31 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def api_teamcity_webhook(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2023-08-12 09:34:31 +02:00
|
|
|
*,
|
2023-09-27 19:01:31 +02:00
|
|
|
payload: JsonBodyPayload[WildValue],
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2022-06-18 16:36:56 +02:00
|
|
|
if "build" not in payload:
|
2020-12-23 10:00:24 +01:00
|
|
|
# Ignore third-party specific (e.g. Slack) payload formats
|
2018-08-01 21:03:30 +02:00
|
|
|
# and notify the bot owner
|
2022-06-18 16:36:56 +02:00
|
|
|
error_message = MISCONFIGURED_PAYLOAD_TYPE_ERROR_MESSAGE.format(
|
2018-08-01 21:03:30 +02:00
|
|
|
bot_name=user_profile.full_name,
|
|
|
|
support_email=FromAddress.SUPPORT,
|
|
|
|
).strip()
|
2022-06-18 16:36:56 +02:00
|
|
|
send_rate_limited_pm_notification_to_bot_owner(
|
|
|
|
user_profile, user_profile.realm, error_message
|
|
|
|
)
|
2018-08-01 21:03:30 +02:00
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2022-06-18 16:36:56 +02:00
|
|
|
message = payload.get("build")
|
|
|
|
build_name = message["buildFullName"].tame(check_string)
|
|
|
|
build_url = message["buildStatusUrl"].tame(check_string)
|
2021-02-12 08:20:45 +01:00
|
|
|
changes_url = build_url + "&tab=buildChangesDiv"
|
2022-06-18 16:36:56 +02:00
|
|
|
build_number = message["buildNumber"].tame(check_string)
|
|
|
|
build_result = message["buildResult"].tame(check_string)
|
|
|
|
build_result_delta = message["buildResultDelta"].tame(check_string)
|
|
|
|
build_status = message["buildStatus"].tame(check_string)
|
2021-02-12 08:20:45 +01:00
|
|
|
|
|
|
|
if build_result == "success":
|
|
|
|
if build_result_delta == "fixed":
|
|
|
|
status = "has been fixed! :thumbs_up:"
|
2016-04-21 19:18:41 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
status = "was successful! :thumbs_up:"
|
|
|
|
elif build_result == "failure":
|
|
|
|
if build_result_delta == "broken":
|
|
|
|
status = f"is broken with status {build_status}! :thumbs_down:"
|
2016-04-21 19:18:41 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
status = f"is still broken with status {build_status}! :thumbs_down:"
|
|
|
|
elif build_result == "running":
|
|
|
|
status = "has started."
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2019-05-09 18:42:50 +02:00
|
|
|
template = """
|
|
|
|
{build_name} build {build_id} {status} See [changes]\
|
|
|
|
({changes_url}) and [build log]({log_url}).
|
|
|
|
""".strip()
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2019-05-09 18:42:50 +02:00
|
|
|
body = template.format(
|
|
|
|
build_name=build_name,
|
|
|
|
build_id=build_number,
|
|
|
|
status=status,
|
|
|
|
changes_url=changes_url,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
log_url=build_url,
|
2019-05-09 18:42:50 +02:00
|
|
|
)
|
2018-06-06 16:29:49 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "branchDisplayName" in message:
|
2024-01-17 15:53:30 +01:00
|
|
|
topic_name = "{} ({})".format(build_name, message["branchDisplayName"].tame(check_string))
|
2018-06-06 16:29:49 +02:00
|
|
|
else:
|
2024-01-17 15:53:30 +01:00
|
|
|
topic_name = build_name
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2023-06-19 16:34:52 +02:00
|
|
|
# Check if this is a personal build, and if so try to direct message the user who triggered it.
|
2021-02-12 08:19:30 +01:00
|
|
|
if (
|
2021-02-12 08:20:45 +01:00
|
|
|
get_teamcity_property_value(message["teamcityProperties"], "env.BUILD_IS_PERSONAL")
|
|
|
|
== "true"
|
2021-02-12 08:19:30 +01:00
|
|
|
):
|
2017-11-05 02:48:25 +01:00
|
|
|
# The triggeredBy field gives us the teamcity user full name, and the
|
|
|
|
# "teamcity.build.triggeredBy.username" property gives us the teamcity username.
|
|
|
|
# Let's try finding the user email from both.
|
2022-06-18 16:36:56 +02:00
|
|
|
teamcity_fullname = message["triggeredBy"].tame(check_string).split(";")[0]
|
2016-04-21 19:18:41 +02:00
|
|
|
teamcity_user = guess_zulip_user_from_teamcity(teamcity_fullname, user_profile.realm)
|
|
|
|
|
|
|
|
if teamcity_user is None:
|
2021-02-12 08:19:30 +01:00
|
|
|
teamcity_shortname = get_teamcity_property_value(
|
2021-02-12 08:20:45 +01:00
|
|
|
message["teamcityProperties"], "teamcity.build.triggeredBy.username"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2016-04-21 19:18:41 +02:00
|
|
|
if teamcity_shortname is not None:
|
2021-02-12 08:19:30 +01:00
|
|
|
teamcity_user = guess_zulip_user_from_teamcity(
|
|
|
|
teamcity_shortname, user_profile.realm
|
|
|
|
)
|
2016-04-21 19:18:41 +02:00
|
|
|
|
|
|
|
if teamcity_user is None:
|
|
|
|
# We can't figure out who started this build - there's nothing we can do here.
|
2021-02-12 08:19:30 +01:00
|
|
|
logging.info(
|
2021-12-17 05:13:07 +01:00
|
|
|
"TeamCity webhook couldn't find a matching Zulip user for "
|
|
|
|
"TeamCity user '%s' or '%s'",
|
2021-02-12 08:19:30 +01:00
|
|
|
teamcity_fullname,
|
|
|
|
teamcity_shortname,
|
|
|
|
)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2020-06-09 00:25:09 +02:00
|
|
|
body = f"Your personal build for {body}"
|
2021-08-21 19:24:20 +02:00
|
|
|
client = RequestNotes.get_notes(request).client
|
2021-07-09 18:10:51 +02:00
|
|
|
assert client is not None
|
|
|
|
check_send_private_message(user_profile, client, teamcity_user, body)
|
2017-10-02 07:24:32 +02:00
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2016-04-21 19:18:41 +02:00
|
|
|
|
2024-01-17 15:53:30 +01:00
|
|
|
check_send_webhook_message(request, user_profile, topic_name, body)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|