Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
slack: Allow multiple channels
Browse files Browse the repository at this point in the history
Closes #35
  • Loading branch information
Matous Dzivjak authored and underyx committed Apr 3, 2019
1 parent ba8871c commit 5a56b92
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 60 deletions.
2 changes: 1 addition & 1 deletion crane/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def strip_trailing_slash(_, param, value):
@click.option('--sleep-after-upgrade', envvar='CRANE_SLEEP_AFTER_UPGRADE', default=0, help='seconds to wait after upgrade', show_default=True)
@click.option('--manual-finish', envvar='CRANE_MANUAL_FINISH', default=False, is_flag=True, help='skip automatic upgrade finish')
@click.option('--slack-token', envvar='CRANE_SLACK_TOKEN', default=None, help='Slack API token')
@click.option('--slack-channel', envvar='CRANE_SLACK_CHANNEL', default=None, help='Slack channel to announce in')
@click.option('--slack-channel', envvar='CRANE_SLACK_CHANNEL', default=None, multiple=True, help='Slack channel to announce in')
@click.option('--slack-link', envvar='CRANE_SLACK_LINK', multiple=True, type=(str, str), metavar='TITLE URL', help='links to mention in Slack')
@click.option('--sentry-webhook', envvar='CRANE_SENTRY_WEBHOOK', default=None, help='Sentry release webhook URL', callback=strip_trailing_slash)
@click.option('--webhook-url', envvar='CRANE_WEBHOOK_URL', default=None, multiple=True, help='URLs to POST the release status to', callback=strip_trailing_slash)
Expand Down
112 changes: 66 additions & 46 deletions crane/hooks/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ def aslist(cls, obj):
class Hook(Base):
def __init__(self):
self.token = settings["slack_token"]
self.slack_channel = settings["slack_channel"]
self.slack_channels = settings["slack_channel"]
# The upcoming line is the most ridiculous, stupid, and effective hack I've ever written.
# We create a link that has only a space as its link text, so it doesn't show up in Slack.
# This allows us to store data in a fake URL, instead of needing a database or something.
# Ridiculous.
self.deployment_text = f"<{deployment.id}.com| >"

if self.token and self.slack_channel:
if self.token and self.slack_channels:
users_response = session.get(
"https://slack.com/api/users.list", params={"token": self.token}
)
Expand All @@ -85,15 +85,16 @@ def __init__(self):
channel["name"]: channel["id"]
for channel in channels_response.json()["channels"]
}
self.channel_id = self.channels_by_name[settings["slack_channel"]]
self.channel_ids = [
self.channels_by_name[channel] for channel in self.slack_channels
]

@property
def base_data(self):
return {"token": self.token, "channel": self.channel_id}
def base_data(self, channel_id):
return {"token": self.token, "channel": channel_id}

def get_existing_message(self):
def get_existing_message(self, channel_id):
response = session.get(
"https://slack.com/api/channels.history", params=self.base_data
"https://slack.com/api/channels.history", params=self.base_data(channel_id)
)
messages = response.json()["messages"]
for message in messages:
Expand All @@ -103,6 +104,12 @@ def get_existing_message(self):
)
return message

def get_existing_messages(self):
return {
channel_id: self.get_existing_message(channel_id)
for channel_id in self.channel_ids
}

@staticmethod
def generate_cc_message(commit_msg):
result = ",".join(
Expand Down Expand Up @@ -157,7 +164,7 @@ def generate_new_message(self):
],
}

def send_message(self, message):
def send_message(self, channel_id, message):
fields = message["attachments"][0]["fields"]

if ":x:" in fields["Environment"]:
Expand All @@ -175,13 +182,15 @@ def send_message(self, message):
message["parse"] = True
else:
url = "https://slack.com/api/chat.postMessage"
session.post(url, data={**self.base_data, **message, "link_names": "1"})
session.post(
url, data={**self.base_data(channel_id), **message, "link_names": "1"}
)

def send_reply(self, message_id, text, in_channel=False):
def send_reply(self, channel_id, message_id, text, in_channel=False):
session.post(
"https://slack.com/api/chat.postMessage",
data={
**self.base_data,
**self.base_data(channel_id),
"thread_ts": message_id,
"text": text,
"reply_broadcast": "true" if in_channel else "false",
Expand Down Expand Up @@ -209,54 +218,65 @@ def generate_env_lines(self, env_lines, status):
env_lines.append(status + " " + self.env_text)

def before_upgrade(self):
message = self.get_existing_message() or self.generate_new_message()
fields = message["attachments"][0]["fields"]
messages = self.get_existing_messages() or {
channel_id: self.generate_new_message() for channel_id in self.channel_ids
}

if "ts" in message:
self.send_reply(message["ts"], f"Starting release on {self.env_text}.")
for channel_id, message in messages.items():
fields = message["attachments"][0]["fields"]

self.set_status(message, ":spinner:")
if "ts" in message:
self.send_reply(
channel_id, message["ts"], f"Starting release on {self.env_text}."
)

releaser = self.users_by_email.get(
environ["GITLAB_USER_EMAIL"], environ["GITLAB_USER_EMAIL"]
)
if fields["Releaser"] and releaser.strip("@") not in fields["Releaser"]:
fields["Releaser"] += " & " + releaser
else:
fields["Releaser"] = releaser
self.set_status(message, ":spinner:")

fields["Branch"] = (
(":warning: " if environ["CI_COMMIT_REF_NAME"] != "master" else "")
+ f'<{environ["CI_PROJECT_URL"]}/tree/{environ["CI_COMMIT_REF_NAME"]}|{environ["CI_COMMIT_REF_NAME"]}>'
)
releaser = self.users_by_email.get(
environ["GITLAB_USER_EMAIL"], environ["GITLAB_USER_EMAIL"]
)
if fields["Releaser"] and releaser.strip("@") not in fields["Releaser"]:
fields["Releaser"] += " & " + releaser
else:
fields["Releaser"] = releaser

fields["Branch"] = (
(":warning: " if environ["CI_COMMIT_REF_NAME"] != "master" else "")
+ f'<{environ["CI_PROJECT_URL"]}/tree/{environ["CI_COMMIT_REF_NAME"]}|{environ["CI_COMMIT_REF_NAME"]}>'
)

self.send_message(message)
self.send_message(channel_id, message)

def after_upgrade_success(self):
message = self.get_existing_message()
if not message:
messages = self.get_existing_messages()
if not messages:
return # we didn't even start
self.set_status(message, ":white_check_mark:")
self.send_message(message)
self.send_reply(message["ts"], f"Released on {self.env_text}.")
for channel_id, message in messages.items():
self.set_status(message, ":white_check_mark:")
self.send_message(channel_id, message)
self.send_reply(channel_id, message["ts"], f"Released on {self.env_text}.")

def after_upgrade_failure(self):
message = self.get_existing_message()
if not message:
messages = self.get_existing_messages()
if not messages:
return # we didn't even start
self.set_status(message, ":x:")
self.send_message(message)
self.send_reply(
message["ts"], f"Release failed on {self.env_text}.", in_channel=True
)
for channel_id, message in messages.items():
self.set_status(message, ":x:")
self.send_message(channel_id, message)
self.send_reply(
channel_id,
message["ts"],
f"Release failed on {self.env_text}.",
in_channel=True,
)

@property
def is_active(self):
provided = missing = None
if settings.get("slack_token") and not settings.get("slack_channel"):
provided, missing = "API token", "channel"
elif settings.get("slack_channel") and not settings.get("slack_token"):
provided, missing = "channel", "API token"
if self.token and not self.slack_channels:
provided, missing = "API token", "channels"
elif self.slack_channels and not self.token:
provided, missing = "channels", "API token"

if missing:
click.secho(
Expand All @@ -267,7 +287,7 @@ def is_active(self):
fg="yellow",
)

return settings.get("slack_token") and settings.get("slack_channel")
return self.token and self.slack_channels

@property
def env_text(self):
Expand Down
58 changes: 45 additions & 13 deletions tests/integration/test_hooks_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import tempfile
import git
import requests
import requests_mock
from datetime import datetime
from git import Actor

Expand All @@ -29,7 +28,7 @@ def repo():
@pytest.fixture(autouse=True)
def click_settings(monkeypatch):
monkeypatch.setitem(settings, "slack_token", "xoxp-123-456")
monkeypatch.setitem(settings, "slack_channel", "general")
monkeypatch.setitem(settings, "slack_channel", ["general", "team"])
monkeypatch.setitem(settings, "slack_link", "")
monkeypatch.setitem(settings, "url", "asd")
monkeypatch.setitem(settings, "env", "asd")
Expand All @@ -44,7 +43,9 @@ def mock_slack_api(requests_mock):
)
requests_mock.get(
"https://slack.com/api/channels.list",
json={"channels": [{"name": "general", "id": "123"}]},
json={
"channels": [{"name": "general", "id": "123"}, {"name": "team", "id": "42"}]
},
)


Expand All @@ -65,13 +66,13 @@ def test_get_existing_message(monkeypatch, mocker, repo, slack_response, result)
fake_deployment = Deployment(repo=repo, new_version="HEAD", old_version=old_version)
monkeypatch.setattr(uut, "deployment", fake_deployment)
slack_hook = uut.Hook()
slack_hook.channel_id = "asd"
slack_hook.channel_ids = ["123"]
fake_get = mocker.patch.object(uut.session, "get")
fake_response = mocker.Mock()
fake_response.json = lambda: {"messages": []}
fake_get.return_value = fake_response

assert slack_hook.get_existing_message() is None
assert slack_hook.get_existing_message("123") is None

deployment_id = f"<{uut.deployment.id}.com| >"

Expand All @@ -81,12 +82,43 @@ def test_get_existing_message(monkeypatch, mocker, repo, slack_response, result)
{"text": "colemak", "attachments": [{"fields": []}]},
]
}
assert slack_hook.get_existing_message()["text"] == deployment_id
assert slack_hook.get_existing_message()["attachments"][0][
assert slack_hook.get_existing_message("123")["text"] == deployment_id
assert slack_hook.get_existing_message("123")["attachments"][0][
"fields"
] == uut.AttachmentFields([])


@pytest.mark.parametrize(["slack_response", "result"], [[{"messages": []}, None]])
def test_get_existing_messages(monkeypatch, mocker, repo, slack_response, result):
old_version = repo.head.commit.hexsha
for commit in ["1"]:
repo.index.commit(commit, author=Actor("test_author", "test@test.com"))

fake_deployment = Deployment(repo=repo, new_version="HEAD", old_version=old_version)
monkeypatch.setattr(uut, "deployment", fake_deployment)
slack_hook = uut.Hook()
slack_hook.channel_ids = ["123", "asd"]
fake_get = mocker.patch.object(uut.session, "get")
fake_response = mocker.Mock()
fake_response.json = lambda: {"messages": []}
fake_get.return_value = fake_response

assert slack_hook.get_existing_messages() == {"123": None, "asd": None}

deployment_id = f"<{uut.deployment.id}.com| >"

fake_response.json = lambda: {
"messages": [
{"text": deployment_id, "attachments": [{"fields": []}]},
{"text": "colemak", "attachments": [{"fields": []}]},
]
}
messages = slack_hook.get_existing_messages()
assert len(messages) == 2
assert messages["123"]["text"] == deployment_id
assert messages["asd"]["attachments"][0]["fields"] == uut.AttachmentFields([])


@pytest.mark.parametrize(
["commits", "expected"],
[
Expand Down Expand Up @@ -284,9 +316,9 @@ def test_send_message(monkeypatch, mocker, repo, message_title, url, result_mess
fake_post = mocker.patch.object(requests.Session, "post")

slack_hook = uut.Hook()
slack_hook.channel_id = "asd"
slack_hook.channel_ids = ["asd"]
slack_hook.send_message(
{"attachments": [{"fields": AttachmentFields([message_title])}]}
"asd", {"attachments": [{"fields": AttachmentFields([message_title])}]}
)
base_data = {"token": "xoxp-123-456", "channel": "asd"}
fake_post.assert_called_with(
Expand All @@ -306,8 +338,8 @@ def test_send_reply(monkeypatch, mocker, repo, message_id, text):
monkeypatch.setattr(uut, "deployment", fake_deployment)

slack_hook = uut.Hook()
slack_hook.channel_id = "asd"
slack_hook.send_reply(message_id, text)
slack_hook.channel_ids = ["asd"]
slack_hook.send_reply("asd", message_id, text)

fake_post.assert_called_with(
"https://slack.com/api/chat.postMessage",
Expand Down Expand Up @@ -340,7 +372,7 @@ def test_set_status(monkeypatch, repo, environment_before, environment_after, ex
message = {"attachments": [{"fields": {"Environment": environment_before}}]}

slack_hook = uut.Hook()
slack_hook.channel_id = "asd"
slack_hook.channel_ids = ["asd"]
slack_hook.set_status(message, environment_after)

assert message["attachments"][0]["fields"]["Environment"] == expected
Expand All @@ -355,7 +387,7 @@ def test_is_active__active():
["missing_setting", "expected_error"],
[
("slack_token", "forgot about setting the API token"),
("slack_channel", "forgot about setting the channel"),
("slack_channel", "forgot about setting the channels"),
],
)
def test_is_active__missing_one(missing_setting, expected_error, monkeypatch, mocker):
Expand Down

0 comments on commit 5a56b92

Please sign in to comment.