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

feat: init slack integration #12

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
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