Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gh release to notion and slack #70

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-on_pr_main_bash.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ env:
TBTT_OVH_APP_SECRET: b
TBTT_OVH_CONSUMER_KEY: c
TBTT_GODP_AUTH_TOKEN: d
TBTT_NOTION_API_KEY: e

jobs:
lint-and-test:
Expand Down
12 changes: 12 additions & 0 deletions config/github.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,17 @@
"board_views": {
"default": 3,
"dev-team-escalation": 3
},
"repoNameToReadable": {
"TB-TT": "TB-TT",
"nodejs-treeshaker": "RUCSS",
"saas-director": "SaaS Director",
"monies": "Monies",
"imagify": "Imagify app",
"rocket-cdn": "RocketCDN",
"wp-rocket.me": "WP Rocket website",
"imagify-website": "Imagify website",
"rocketcdn-website": "RocketCDN website"

}
}
3 changes: 3 additions & 0 deletions config/notion.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"release-note-db-id": "9bf76ffc56074d1d9ca81b65ce00f4da"
}
5 changes: 4 additions & 1 deletion config/slack.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"dev-team-escalation-channel": "C056ZJMHG0P"
"dev-team-escalation-channel": "C056ZJMHG0P",
"engineering-service-team-channel": "C069W48E47N",
"release-channel" : "C05PGTQHHJ9",
"ops-channel": "C88N0811V"
}
1 change: 1 addition & 0 deletions sources/TechTeamBot.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __setup_keys(self):
self.__load_key("TBTT_OVH_APP_SECRET", cst.APP_CONFIG_TOKEN_OVH_APP_SECRET)
self.__load_key("TBTT_OVH_CONSUMER_KEY", cst.APP_CONFIG_TOKEN_OVH_CONSUMER_KEY)
self.__load_key("TBTT_GODP_AUTH_TOKEN", cst.APP_CONFIG_TOKEN_GODP_AUTH_TOKEN)
self.__load_key("TBTT_NOTION_API_KEY", cst.APP_CONFIG_TOKEN_NOTION_API_KEY)

def __setup_slack_interaction_endpoint(self):
"""
Expand Down
132 changes: 132 additions & 0 deletions sources/factories/NotionFactory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
This module defines the factory for Notion API
"""
from datetime import date
import json
from pathlib import Path
import requests
from flask import current_app
import sources.utils.Constants as cst
from sources.models.GithubReleaseParam import GithubReleaseParam


class NotionFactory():
"""
Class managing the API for OVH

"""
def __init__(self):
"""
The factory instanciates the objects it needed to complete the processing of the request.
"""
self.api_key = None
with open(Path(__file__).parent.parent.parent / "config" / "notion.json", encoding='utf-8') as file_notion_config:
self.notion_config = json.load(file_notion_config)

def _get_notion_api_key(self, app_context):
"""
Return the Notion API key and creates it if needed
"""
if self.api_key is None:
app_context.push()
self.api_key = current_app.config[cst.APP_CONFIG_TOKEN_NOTION_API_KEY]
return self.api_key

def _create_notion_db_row(self, app_context, db_id, properties, children):
"""
Requests the creation of a row in a Notion DB through the API.
Properties should match the DB columns, and children is the content of the created page.
"""
headers = {
'Authorization': 'Bearer ' + self._get_notion_api_key(app_context),
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
}

data = {
'parent': {'database_id': db_id},
'properties': properties,
'children': children
}

response = requests.post(
'https://api.notion.com/v1/pages',
headers=headers,
json=data,
timeout=3000
)

if response.status_code != 200:
raise ValueError('Notion API could not create the DB row.')
page_id = response.json().get('id')
page_url = f'https://www.notion.so/{page_id}'
return page_url

def create_release_note(self, app_context, release_params: GithubReleaseParam):
"""
Creates a release note in Notion for a GitHub release.
"""
today = date.today()

properties = {
"Version": {
"title": [
{
"text": {
"content": release_params.version
}
}
]
},
"Product": {
"select": {
"name": release_params.repository_name
}
},
'date_property': {
'date': {
'start': today.strftime("%Y-%m-%d")
}
}
}

content = [
{
"object": "block",
"heading_2": {
"rich_text": [
{
"text": {
"content": "Complete Changelog"
}
}
]
}
},
{
"object": "block",
"paragraph": {
"rich_text": [
{
"text": {
"content": release_params.body
},
}
],
"color": "default"
}
},
{
"object": "block",
"heading_2": {
"rich_text": [
{
"text": {
"content": "User notes"
}
}
]
}
}
]
return self._create_notion_db_row(app_context, self.notion_config["release-note-db-id"], properties, content)
45 changes: 44 additions & 1 deletion sources/factories/SlackMessageFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self):
self.update_message_url = 'https://slack.com/api/chat.update'
self.search_message_url = 'https://slack.com/api/search.messages'

def post_message(self, app_context, channel, text):
def post_message(self, app_context, channel, text, blocks=None):
"""
Sends a message 'text' to the 'channel' as the app.
"""
Expand All @@ -33,6 +33,10 @@ def post_message(self, app_context, channel, text):
request_open_view_payload = {}
request_open_view_payload['channel'] = channel
request_open_view_payload['text'] = text

if blocks is not None:
request_open_view_payload['blocks'] = blocks

result = requests.post(url=self.post_message_url,
headers=request_open_view_header,
json=request_open_view_payload, timeout=3000)
Expand Down Expand Up @@ -99,4 +103,43 @@ def get_channel(self, flow):
"""
if 'dev-team-escalation' == flow:
return self.slack_config["dev-team-escalation-channel"]
if 'engineering-service-team' == flow:
return self.slack_config["engineering-service-team-channel"]
if 'releases' == flow:
return self.slack_config["release-channel"]
if 'ops' == flow:
return self.slack_config["ops-channel"]
raise ValueError('Unknown flow for get_channel.')

def get_release_note_review_blocks(self, text):
"""
Build the interactive messages for the release note publication flow
"""
blocks = [
{
"type": "section",
"text": {
"type": "plain_text",
"text": text,
"emoji": True
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Publish the release note in the release channel?"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Yes!",
"emoji": True
},
"value": text,
"action_id": "publish-release-note"
}
}
]
return blocks
5 changes: 4 additions & 1 deletion sources/handlers/DeployHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
This module defines the handler for deployment with the group.One Deploy Proxy
This handler is just a API call factory, as there is no special business logic.
"""
import json
import requests
from flask import current_app
import json
import sources.utils.Constants as cst
from sources.models.DeployHandlerParam import DeployHandlerParam

Expand Down Expand Up @@ -32,6 +32,9 @@ def _get_godp_token(self, app_context):
return self.__godp_token

def deploy_commit(self, app_context, task_params: DeployHandlerParam):
"""
Triggers a deployment by calling the GODP API endpoint
"""
app_context.push()
request_header = {"Content-type": "application/json",
"Authorization": "Bearer " + self._get_godp_token(app_context)}
Expand Down
42 changes: 42 additions & 0 deletions sources/handlers/GithubReleaseHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
This module defines the handler for GitHub Release related logic.
"""
import json
from pathlib import Path
from sources.factories.SlackMessageFactory import SlackMessageFactory
from sources.factories.NotionFactory import NotionFactory


class GithubReleaseHandler():
"""
Class managing the business logic related to Github releases

"""
def __init__(self):
"""
The handler instanciates the objects it needed to complete the processing of the request.
"""
self.slack_message_factory = SlackMessageFactory()
self.notion_factory = NotionFactory()
with open(Path(__file__).parent.parent.parent / "config" / "github.json", encoding='utf-8') as file_github_config:
self.github_config = json.load(file_github_config)

def process_release(self, app_context, release_params):
"""
Processing method when a github release is released
"""
# Replace the repository name by its readable name
repository_readable_name = self.github_config["repoNameToReadable"][release_params.repository_name]
release_params.repository_name = repository_readable_name
# Create a page in the Notion database
notion_url = self.notion_factory.create_release_note(app_context, release_params)

# Send a message to Slack
text = "The draft release note for " + repository_readable_name + " " + release_params.version
text += " is available on Notion: " + notion_url

blocks = self.slack_message_factory.get_release_note_review_blocks(text)

self.slack_message_factory.post_message(app_context,
self.slack_message_factory.get_channel('ops'),
text, blocks)
30 changes: 30 additions & 0 deletions sources/handlers/GithubWebhookHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from pathlib import Path
from flask import current_app
from sources.handlers.GithubTaskHandler import GithubTaskHandler
from sources.handlers.GithubReleaseHandler import GithubReleaseHandler
from sources.models.GithubReleaseParam import GithubReleaseParam


class GithubWebhookHandler():
Expand All @@ -18,6 +20,7 @@ def __init__(self):
The handler instanciates the objects it needed to complete the processing of the request.
"""
self.github_project_item_handler = GithubTaskHandler()
self.github_release_handler = GithubReleaseHandler()
with open(Path(__file__).parent.parent.parent / "config" / "github.json", encoding='utf-8') as file_github_config:
self.github_config = json.load(file_github_config)

Expand All @@ -28,6 +31,8 @@ def process(self, payload_json):
"""
if "projects_v2_item" in payload_json:
self.project_v2_item_update_callback(payload_json)
elif "release" in payload_json:
self.release_callback(payload_json)
else:
raise ValueError('Unknown webhook payload.')
return {}
Expand All @@ -53,3 +58,28 @@ def project_v2_item_update_callback(self, payload_json):
"app_context": current_app.app_context(), "node_id": node_id})
current_app.logger.info("project_v2_item_update_callback: Starting processing thread...")
thread.start()

def release_callback(self, payload_json):
"""
Callback for webhooks linked to a Github release.
Filter out irrelevant webhooks.
Retrieve the relevant data in paylaod and start a thread for further processing
"""
# Keep only released actions
if "action" not in payload_json or "released" != payload_json["action"]:
current_app.logger.info("release_callback: Not a released release action.")
return
# Keep only changes on status or assignees
repository_name = payload_json["repository"]["name"]
if repository_name not in self.github_config["repoNameToReadable"]:
current_app.logger.info("release_callback: Release not linked to a tracked repository.")
return

version = payload_json["release"]["tag_name"]
body = payload_json["release"]["body"]

release_params = GithubReleaseParam(repository_name, version, body)
thread = Thread(target=self.github_release_handler.process_release, kwargs={
"app_context": current_app.app_context(), "release_params": release_params})
current_app.logger.info("release_callback: Starting processing thread...")
thread.start()
Loading