Skip to content

Commit

Permalink
feat: init slack integration (#12)
Browse files Browse the repository at this point in the history
* feat: init slack integration

* test commit - update docs

* fixes and tidy

* update documentation

* update cli

* update cli override and github action

* update default slack headline

* extend secret key printing length

* documentation

---------

Co-authored-by: Thomas Greenwood <[email protected]>
Co-authored-by: addepar-tg <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 4bfff92 commit bb32653
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.DS_Store

logs/
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ export RF_JIRA_USER=your-username-here
export RF_JIRA_TOKEN=your-token-here
```

##### Slack Token *(Optional)*

1. [Create a Slack App](https://api.slack.com/quickstart)
1. [Request required scopes](https://api.slack.com/quickstart#scopes)
1. [Install and authorize the App](https://api.slack.com/quickstart#installing)
1. Add environment variables or configuration entries when running RedFlag

```shell
export RF_SLACK_TOKEN=xoxb-slack-token-here
export RF_SLACK_CHANNEL=C0123456789
```

*Don't forget to invite the bot to the channel to avoid a `channel_not_found` error*.
### Usage
Here are some examples on how to run RedFlag in batch mode.
Expand Down Expand Up @@ -139,7 +153,7 @@ By default, RedFlag produces an HTML report that can be opened in a browser.
RedFlag can be run in CI pipelines to flag PRs and add the appropriate reviewers.
This mode uses GitHub Actions to run RedFlag on every PR and post a comment if
the PR requires a review.
the PR requires a review. Additionally, CI Mode is best suited for Slack alerting.
[![CI Mode][docs-ci-mode]][docs-ci-mode-url]
Expand Down Expand Up @@ -186,12 +200,15 @@ The following table shows configuration options for each parameter:
#### Integration Settings
| Parameter | CLI Param | Env Var | Config File | Default |
|-------------------------------------------------------------------------------------------------------------------------------------|----------------|------------------|---------------|---------|
| [GitHub Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) | --github-token | RF_GITHUB_TOKEN | github_token | - |
| Jira URL | --jira-url | RF_JIRA_URL | jira.url | - |
| Jira Username | --jira-user | RF_JIRA_USER | jira.user | - |
| [Jira Token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) | --jira-token | RF_JIRA_TOKEN | jira.token | - |
| Parameter | CLI Param | Env Var | Config File | Default |
|-------------------------------------------------------------------------------------------------------------------------------------|------------------|-------------------|---------------|---------|
| [GitHub Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) | --github-token | RF_GITHUB_TOKEN | github_token | - |
| Jira URL | --jira-url | RF_JIRA_URL | jira.url | - |
| Jira Username | --jira-user | RF_JIRA_USER | jira.user | - |
| [Jira Token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) | --jira-token | RF_JIRA_TOKEN | jira.token | - |
| [Slack Token](https://api.slack.com/concepts/token-types) | --slack-token | RF_SLACK_TOKEN | slack.token | - |
| [Slack Channel (ID)](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID) | --slack-channel | RF_SLACK_CHANNEL | slack.channel | - |
| Slack Message Headline | --slack-headline | RF_SLACK_HEADLINE | slack.headline | - |
#### LLM Settings
Expand Down
17 changes: 17 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ inputs:
type: string
required: false
description: 'Username to use for the Jira integration.'
slack_token:
type: string
required: false
description: 'Bot token for authenticating to Slack.'
slack_channel:
type: string
required: false
description: 'Channel ID for posting to Slack.'
slack_headline:
type: string
required: false
description: 'Message headline for posting to Slack.'
reviewer_teams:
type: string
required: false
Expand Down Expand Up @@ -82,6 +94,8 @@ runs:
RF_GITHUB_TOKEN: ${{ inputs.github_token }}
RF_JIRA_TOKEN: ${{ inputs.jira_token }}
RF_FROM: ""
RF_SLACK_TOKEN: ${{ inputs.slack_token }}
RF_SLACK_CHANNEL: ${{ inputs.slack_channel }}
working-directory: ${{ github.action_path }}
shell: bash
run: |
Expand All @@ -108,6 +122,9 @@ runs:
if [ -n "${{ inputs.jira_user }}" ]; then
cli_opts+=" --jira-user ${{ inputs.jira_user }}"
fi
if [ -n "${{ inputs.slack_headline }}" ]; then
cli_opts+=" --slack-headline ${{ inputs.slack_headline }}"
fi
if [ -n "${{ inputs.config_file }}" ]; then
cli_opts+=" --config config-active.yaml"
fi
Expand Down
27 changes: 27 additions & 0 deletions addepar_redflag/redflag.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
build_prompt,
MAX_PARSER_RETRIES
)
from .util.slack import Slack


async def query_model(
Expand Down Expand Up @@ -137,6 +138,7 @@ async def query_model(
async def redflag(
github: Github,
jira: Jira,
slack: Slack,
config: dict,
):
try:
Expand Down Expand Up @@ -505,6 +507,31 @@ async def redflag(
MessageType.SUCCESS
)

# If Slack client is configured, send the results
if slack:
# Check if there are in-scope items
if in_scope:
blocks = slack.build_slack_blocks(
config.get('slack').get('headline'),
{
"in_scope": in_scope,
"out_of_scope": out_of_scope,
}
)

if blocks:
slack.post_message(blocks)

pretty_print(
f'Successfully sent message to Slack in channel #{slack.channel}',
MessageType.SUCCESS
)
else:
pretty_print(
'No reviews to send to Slack',
MessageType.INFO
)

if errored:
file_path = Path(output_dir or '.') / f'Errors-{filename}.json'
with open(file_path, 'w') as f:
Expand Down
23 changes: 23 additions & 0 deletions addepar_redflag/util/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from atlassian import Jira
from dotenv import load_dotenv
from github import Auth, Github
from .slack import Slack
from langchain.globals import set_debug

from ..evaluate import do_evaluations
Expand Down Expand Up @@ -54,6 +55,9 @@ def cli():
parser.add_argument('--max-commits', type=int, help=f'The max number of commits to feed to the LLM. (default: {default_config["max_commits"]})')
parser.add_argument('--no-output-html', action='store_false', dest='output_html', help='Flag to not output the results as HTML.')
parser.add_argument('--no-output-json', action='store_false', dest='output_json', help='Flag to not output the results as JSON.')
parser.add_argument('--slack-token', help='Slack OAuth token to authenticate to the Slack API.')
parser.add_argument('--slack-channel', help='Slack channel ID to post messages to.')
parser.add_argument('--slack-headline', help='Slack message headline.')
common_arguments(parser, default_config)

# Eval subparser
Expand Down Expand Up @@ -96,6 +100,24 @@ def cli():
password=jira_token
)

# Instantiate Slack object
slack = None
if final_config['slack']['channel']:
slack_channel = final_config['slack']['channel']
slack_token = final_config['slack']['token']

if not (slack_token and slack_channel):
pretty_print(
'Slack auth tokens and a channel ID are required for this operation. To skip the Slack integration, leave --slack-token and --slack-channel blank.',
MessageType.FATAL
)
exit(1)

slack = Slack(
token=slack_token,
channel=slack_channel
)

# Debug LLM output
if final_config['debug_llm']:
set_debug(True)
Expand All @@ -114,6 +136,7 @@ def cli():
asyncio.run(redflag(
github=github,
jira=jira,
slack=slack,
config=final_config
))

Expand Down
15 changes: 10 additions & 5 deletions addepar_redflag/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ def get_default_config():
'profile': None,
'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0'
},
'slack': {
'token': None,
'channel': None,
'headline': '🚩 RedFlag Security Review Alert'
},
'prompts': {
'review': {
'role': DEFAULT_ROLE,
Expand Down Expand Up @@ -146,10 +151,10 @@ def get_final_config(cli_args):
'user': getenv('RF_JIRA_USER'),
'token': getenv('RF_JIRA_TOKEN')
},
'bedrock': {
'region': getenv('RF_BEDROCK_REGION'),
'profile': getenv('RF_BEDROCK_PROFILE'),
'model_id': getenv('RF_BEDROCK_MODEL_ID')
'slack': {
'token': getenv('RF_SLACK_TOKEN'),
'channel': getenv('RF_SLACK_CHANNEL'),
'headline': getenv('RF_SLACK_HEADLINE')
}
}

Expand All @@ -163,7 +168,7 @@ def get_final_config(cli_args):

# Override current config with values from the CLI args
cli_dict = dict()
nested_keys = ['jira', 'bedrock']
nested_keys = ['jira', 'bedrock', 'slack']
default_config = get_default_config()
for key, value in vars(cli_args).items():
if value is not None:
Expand Down
4 changes: 2 additions & 2 deletions addepar_redflag/util/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def pretty_print_config_table(
table.add_column("Value")
table.add_column("Source")

secret_keys = ["github_token", "jira.token"]
secret_keys = ["github_token", "jira.token", "slack.token"]
hidden_items = ["command"]

def add_row(table, key, value, parent_key = None):
Expand All @@ -103,7 +103,7 @@ def add_row(table, key, value, parent_key = None):
# Hide secrets
if full_key in secret_keys:
if value is not None:
value = "*" * len(value)
value = value[:6] + '*' * (len(value) - 4)

# Prettify None values
if value is None:
Expand Down
131 changes: 131 additions & 0 deletions addepar_redflag/util/slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import os
import requests
import json

from .console import (
pretty_print,
MessageType
)


class Slack():
# The __init__ method initializes the Slack token and channel
def __init__(
self,
token,
channel
):
self.token = token
self.channel = channel
self.base_url = "https://slack.com/api/"

# create function for each corresponding slack block kit element from oad_dict written in the main script
def build_title_block(
self,
headline
) -> dict:
return {
"type": "header",
"text": {
"type": "plain_text",
"text": headline,
"emoji": True
}
}

def build_repo_info_block(
self,
repository,
pr_title,
commit_url=None
) -> dict:
text = f"*Repository*: <https://github.com/{repository}|{repository}> \n"

if commit_url:
text += f"*Title*: <{commit_url}|{pr_title}>"

return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}

def build_reasoning_block_in_scope(
self,
reason_in_scope
) -> dict:
return {
"type": "section",
"text":
{
"type": "mrkdwn",
"text": f"\n{reason_in_scope}"
}
}

def build_divider_block(self) -> dict:
return {
"type": "divider"
}

def build_slack_blocks(
self,
headline,
results
) -> list:
blocks = []
in_scope = results["in_scope"]

for obj in in_scope:
if obj is None:
continue

# Extract the pr title nested object and append to the function blocks
title_block = self.build_title_block(headline)
blocks.append(title_block)

# Extract the repo and url nested object and append to the function blocks
pr_title = obj.pr.title
repository = obj.pr.repository
commit_url = obj.pr.url
info_block = self.build_repo_info_block(repository, pr_title, commit_url)
blocks.append(info_block)

# Extract the in_scope reason nested object and append to the function blocks
reason_in_scope = obj.review.reasoning
reason_in_scope_block = self.build_reasoning_block_in_scope(reason_in_scope)
blocks.append(reason_in_scope_block)
#
return blocks


# The post_message method sends a message to the specified Slack channel using the Slack API
def post_message(self, blocks):
headers = {
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {self.token}"
}

payload = {
"channel": self.channel,
"blocks": blocks
}

response = requests.post(
url=f'{self.base_url}chat.postMessage',
headers=headers,
data=json.dumps(payload)
)

# Check if the response status code is not 200 (OK)
if not response.status_code // 100 == 2:
# Log the error or raise an exception
raise Exception(f"Post Slack message failed: {response.status_code} - {response.text}")

# If successful, pretty_print the success message
pretty_print(
f"Posted Slack message successfully in channel #{self.channel}",
MessageType.INFO
)
8 changes: 8 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ to: v0.139.3
# However, if needed, it can be set here.
github_token: token

# Slack token, channel and message header to send messages to a channel
slack:
channel: channel-id
token: token
headline: |
:rotating_light: RedFlag Security Review Alert :rotating_light:
:eyes: Review Requested :eyes:
jira:
# Omit jira_url to skip using Jira.
url:
Expand Down

0 comments on commit bb32653

Please sign in to comment.