Skip to content

Commit

Permalink
Merge pull request #70 from wp-media/feature/GH-release-to-Notion-and…
Browse files Browse the repository at this point in the history
…-Slack

Feature/gh release to notion and slack
  • Loading branch information
MathieuLamiot authored Jan 17, 2024
2 parents 3fd4783 + aced2fa commit fb2939d
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 3 deletions.
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

0 comments on commit fb2939d

Please sign in to comment.