Skip to content

Commit

Permalink
Adds Discord Support & Adds Repo Name to Messages (#18)
Browse files Browse the repository at this point in the history
* Adds Discord support

* Adds repo name to messages

* fix CHANGELOG
  • Loading branch information
Justintime50 committed Dec 5, 2020
1 parent 9a8a097 commit ab272f1
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 310 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,13 @@
# CHANGELOG

## v2.1.0 (2020-12-03)

* Adds Discord support. Now you can send Pullbug messages to a Discord webhook (closes #17)
* Completely rewrote the message module. Messages are now an array of messages built from PR/MR data. This allows messages to be broken up easily into batches for chat services such as Discord which may require multiple batches of messages due to character limit
* The repo name is now included in each message with a link to the repo (closes #16)
* Reworked tests to be more clean and uniform
* Various bug fixes and code refactor for better performance and maintainability

## v2.0.6 (2020-10-10)

* Fixing typo in gitlab message
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -24,7 +24,7 @@ clean:
## lint - Lint the project
lint:
venv/bin/flake8 pullbug/*.py
venv/bin/flake8 test/*.py
venv/bin/flake8 test/unit/*.py

## test - Test the project
test:
Expand Down
10 changes: 6 additions & 4 deletions README.md
Expand Up @@ -2,7 +2,7 @@

# Pullbug 🐛

Get bugged via Slack or RocketChat to merge your GitHub pull requests or GitLab merge requests.
Get bugged via Discord, Slack, or RocketChat to merge your GitHub pull requests or GitLab merge requests.

[![Build Status](https://travis-ci.com/Justintime50/pullbug.svg?branch=master)](https://travis-ci.com/Justintime50/pullbug)
[![Coverage Status](https://coveralls.io/repos/github/Justintime50/pullbug/badge.svg?branch=master)](https://coveralls.io/github/Justintime50/pullbug?branch=master)
Expand All @@ -13,9 +13,9 @@ Get bugged via Slack or RocketChat to merge your GitHub pull requests or GitLab

</div>

Pullbug can notify you on Slack or Rocket.Chat of all open pull and merge requests from GitHub or GitLab. This tool ensures requests never go unnoticed as it can be setup on a schedule to constantly bug you to merge your work. This is perfect for finding old or stale requests and helps you to stay current on new ones. Pass in a few environment variables, setup a [Slackbot](https://slack.com/help/articles/115005265703-Create-a-bot-for-your-workspace) or [Rocket.Chat](https://rocket.chat/docs/developer-guides/rest-api/integration/create/) integration and you're all set to be bugged by Pullbug.
Pullbug can notify you on Discord, Slack, or Rocket.Chat of all open pull and merge requests from GitHub or GitLab. This tool ensures requests never go unnoticed as it can be setup on a schedule to constantly bug you to merge your work. This is perfect for finding old or stale requests and helps you to stay current on new ones. Pass in a few environment variables, setup a [Slackbot](https://slack.com/help/articles/115005265703-Create-a-bot-for-your-workspace) or [Rocket.Chat](https://rocket.chat/docs/developer-guides/rest-api/integration/create/) integration and you're all set to be bugged by Pullbug.

**NOTE:** Pullbug works best if you have link unfurling turned off for GitHub and GitLab on Slack or Rocket.Chat.
**NOTE:** Pullbug works best if you have link unfurling turned off for GitHub and GitLab on Discord, Slack, or Rocket.Chat.

**GitLab Users:** If you are not hosting your own GitLab instance and are instead using `gitlab.com`, it's recommended to change the scope to `owner` and provide an owner who has access to all your organizations merge requests.

Expand All @@ -34,7 +34,7 @@ make help

## Usage

Pullbug works best when run on a schedule. Run one-off reports or setup Pullbug to notify you at whatever interval you'd like to be bugged via Slack or Rocket.Chat about pull or merge requests.
Pullbug works best when run on a schedule. Run one-off reports or setup Pullbug to notify you at whatever interval you'd like to be bugged via Discord, Slack, or Rocket.Chat about pull or merge requests.

Pullbug is highly customizable allowing you to mix and match version control software along with messaging platforms to get the right fit. Additionally choose which kinds of pull or merge requests to retrieve.

Expand All @@ -46,6 +46,7 @@ Options:
-h, --help show this help message and exit
-gh, --github Get bugged about pull requests from GitHub.
-gl, --gitlab Get bugged about merge requests from GitLab.
-d, --discord Send Pullbug messages to Discord.
-s, --slack Send Pullbug messages to Slack.
-rc, --rocketchat Send Pullbug messages to Rocket.Chat.
-w, --wip Include "Work in Progress" pull or merge requests.
Expand All @@ -64,6 +65,7 @@ Environment Variables:
GITHUB_TOKEN The GitHub Token used to authenticate with the GitHub API.
GITLAB_API_KEY The GitLab API Key used to authenticate with the GitLab API.
GITLAB_API_URL The GitLab API url for your GitLab instance. Default: https://gitlab.com/api/v4.
DISCORD_WEBHOOK_URL The Discord webhook url to send a message to. Will look like: https://discord.com/api/webhooks/channel_id/webhook_id
SLACK_BOT_TOKEN The Slackbot Token used to authenticate with Slack.
SLACK_CHANNEL The Slack channel to post a message to.
ROCKET_CHAT_URL The Rocket.Chat url of the room to post a message to.
Expand Down
22 changes: 17 additions & 5 deletions pullbug/cli.py
Expand Up @@ -10,6 +10,7 @@
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
GITLAB_API_KEY = os.getenv('GITLAB_API_KEY')
GITLAB_API_URL = os.getenv('GITLAB_API_URL', 'https://gitlab.com/api/v4')
DISCORD_WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
SLACK_BOT_TOKEN = os.getenv('SLACK_BOT_TOKEN')
SLACK_CHANNEL = os.getenv('SLACK_CHANNEL')
ROCKET_CHAT_URL = os.getenv('ROCKET_CHAT_URL')
Expand Down Expand Up @@ -40,6 +41,14 @@ def __init__(self):
default=False,
help='Get bugged about merge requests from GitLab.'
)
parser.add_argument(
'-d',
'--discord',
required=False,
action='store_true',
default=False,
help='Send Pullbug messages to Discord.'
)
parser.add_argument(
'-s',
'--slack',
Expand Down Expand Up @@ -112,6 +121,7 @@ def run(self):
PullBug.run(
github=self.github,
gitlab=self.gitlab,
discord=self.discord,
slack=self.slack,
rocketchat=self.rocketchat,
wip=self.wip,
Expand All @@ -125,22 +135,22 @@ def run(self):

class PullBug():
@classmethod
def run(cls, github, gitlab, slack, rocketchat, wip, github_owner,
def run(cls, github, gitlab, discord, slack, rocketchat, wip, github_owner,
github_state, github_context, gitlab_state, gitlab_scope):
"""Run Pullbug based on the configuration.
"""
PullBugLogger._setup_logging(LOGGER)
LOGGER.info('Running Pullbug...')
load_dotenv()
cls.run_missing_checks(github, gitlab, slack, rocketchat)
cls.run_missing_checks(github, gitlab, discord, slack, rocketchat)
if github:
GithubBug.run(github_owner, github_state, github_context, wip, slack, rocketchat)
GithubBug.run(github_owner, github_state, github_context, wip, discord, slack, rocketchat)
if gitlab:
GitlabBug.run(gitlab_scope, gitlab_state, wip, slack, rocketchat)
GitlabBug.run(gitlab_scope, gitlab_state, wip, discord, slack, rocketchat)
LOGGER.info('Pullbug finished bugging!')

@classmethod
def run_missing_checks(cls, github, gitlab, slack, rocketchat):
def run_missing_checks(cls, github, gitlab, discord, slack, rocketchat):
"""Check that values are set based on
configuration before proceeding.
"""
Expand All @@ -152,6 +162,8 @@ def run_missing_checks(cls, github, gitlab, slack, rocketchat):
cls.throw_missing_error('GITHUB_TOKEN')
if gitlab and not GITLAB_API_KEY:
cls.throw_missing_error('GITLAB_API_KEY')
if discord and not DISCORD_WEBHOOK_URL:
cls.throw_missing_error('DISCORD_WEBHOOK_URL')
if slack and not SLACK_BOT_TOKEN:
cls.throw_missing_error('SLACK_BOT_TOKEN')
if slack and not SLACK_CHANNEL:
Expand Down
66 changes: 24 additions & 42 deletions pullbug/github_bug.py
Expand Up @@ -15,49 +15,53 @@

class GithubBug():
@classmethod
def run(cls, github_owner, github_state, github_context, wip, slack, rocketchat):
def run(cls, github_owner, github_state, github_context, wip, discord, slack, rocketchat):
"""Run the logic to get PR's from GitHub and
send that data via message.
"""
PullBugLogger._setup_logging(LOGGER)
repos = cls.get_repos(github_owner, github_context)
pull_requests = cls.get_pull_requests(repos, github_owner, github_state)
message_preamble = ''

if pull_requests == []:
message = 'No pull requests are available from GitHub.'
LOGGER.info(message)
return message

message_preamble = '\n:bug: *The following pull requests on GitHub are still open and need your help!*\n'
pull_request_messages = cls.iterate_pull_requests(pull_requests, wip)
final_message = message_preamble + pull_request_messages
messages, discord_messages = cls.iterate_pull_requests(pull_requests, wip, discord, slack, rocketchat)
messages.insert(0, message_preamble)
discord_messages.insert(0, message_preamble)
if discord:
Messages.send_discord_message(discord_messages)
if slack:
Messages.slack(final_message)
Messages.send_slack_message(messages)
if rocketchat:
Messages.rocketchat(final_message)
LOGGER.info(final_message)
Messages.send_rocketchat_message(messages)
LOGGER.info(messages)

@classmethod
def get_repos(cls, github_owner, github_context=''):
"""Get all repos of the GITHUB_OWNER.
"""
LOGGER.info('Bugging GitHub for repos...')
try:
repos_response = requests.get(
response = requests.get(
f'https://api.github.com/{github_context}/{github_owner}/repos?per_page=100',
headers=GITHUB_HEADERS
)
LOGGER.debug(repos_response.text)
if 'Not Found' in repos_response.text:
LOGGER.debug(response.text)
LOGGER.info('GitHub repos retrieved!')
if 'Not Found' in response.text:
error = f'Could not retrieve GitHub repos due to bad parameter: {github_owner} | {github_context}.'
LOGGER.error(error)
raise ValueError(error)
LOGGER.info('GitHub repos retrieved!')
except requests.exceptions.RequestException as response_error:
LOGGER.error(
f'Could not retrieve GitHub repos: {response_error}'
)
raise requests.exceptions.RequestException(response_error)
return repos_response.json()
return response.json()

@classmethod
def get_pull_requests(cls, repos, github_owner, github_state):
Expand Down Expand Up @@ -90,39 +94,17 @@ def get_pull_requests(cls, repos, github_owner, github_state):
return pull_requests

@classmethod
def iterate_pull_requests(cls, pull_requests, wip):
def iterate_pull_requests(cls, pull_requests, wip, discord, slack, rocketchat):
"""Iterate through each pull request of a repo
and send a message to Slack if a PR exists.
and build the message array.
"""
final_message = ''
message_array = []
discord_message_array = []
for pull_request in pull_requests:
if not wip and 'WIP' in pull_request['title'].upper():
continue
else:
message = cls.prepare_message(pull_request)
final_message += message
return final_message

@classmethod
def prepare_message(cls, pull_request):
"""Prepare the message with pull request data.
"""
# TODO: Check requested_reviewers array also
try:
if pull_request['assignees'][0]['login']:
users = ''
for assignee in pull_request['assignees']:
user = f"<{assignee['html_url']}|{assignee['login']}>"
users += user + ' '
else:
users = 'No assignee'
except IndexError:
users = 'No assignee'

# Truncate description after 120 characters
description = (pull_request['body'][:120] + '...') if len(pull_request
['body']) > 120 else pull_request['body']
message = f"\n:arrow_heading_up: *Pull Request:* <{pull_request['html_url']}|" + \
f"{pull_request['title']}>\n*Description:* {description}\n*Waiting on:* {users}\n"

return message
message, discord_message = Messages.prepare_github_message(pull_request, discord, slack, rocketchat)
message_array.append(message)
discord_message_array.append(discord_message)
return message_array, discord_message_array
59 changes: 21 additions & 38 deletions pullbug/gitlab_bug.py
Expand Up @@ -16,25 +16,29 @@

class GitlabBug():
@classmethod
def run(cls, gitlab_scope, gitlab_state, wip, slack, rocketchat):
def run(cls, gitlab_scope, gitlab_state, wip, discord, slack, rocketchat):
"""Run the logic to get MR's from GitLab and
send that data via message.
"""
PullBugLogger._setup_logging(LOGGER)
merge_requests = cls.get_merge_requests(gitlab_scope, gitlab_state)
message_preamble = ''

if merge_requests == []:
message = 'No merge requests are available from GitLab.'
LOGGER.info(message)
return message

message_preamble = '\n:bug: *The following merge requests on GitLab are still open and need your help!*\n'
merge_request_messages = cls.iterate_merge_requests(merge_requests, wip)
final_message = message_preamble + merge_request_messages
messages, discord_messages = cls.iterate_merge_requests(merge_requests, wip, discord, slack, rocketchat)
messages.insert(0, message_preamble)
discord_messages.insert(0, message_preamble)
if discord:
Messages.send_discord_message(discord_messages)
if slack:
Messages.slack(final_message)
Messages.send_slack_message(messages)
if rocketchat:
Messages.rocketchat(final_message)
LOGGER.info(final_message)
Messages.send_rocketchat_message(messages)
LOGGER.info(messages)

@classmethod
def get_merge_requests(cls, gitlab_scope, gitlab_state):
Expand All @@ -46,7 +50,7 @@ def get_merge_requests(cls, gitlab_scope, gitlab_state):
f"{GITLAB_API_URL}/merge_requests?scope={gitlab_scope}&state={gitlab_state}&per_page=100",
headers=GITLAB_HEADERS
)
print(response.json())
LOGGER.debug(response.text)
LOGGER.info('GitLab merge requests retrieved!')
if 'does not have a valid value' in response.text:
error = f'Could not retrieve GitLab merge requests due to bad parameter: {gitlab_scope} | {gitlab_state}.' # noqa
Expand All @@ -60,40 +64,19 @@ def get_merge_requests(cls, gitlab_scope, gitlab_state):
return response.json()

@classmethod
def iterate_merge_requests(cls, merge_requests, wip):
"""Iterate through each merge request and send
a message to Slack if a PR exists.
def iterate_merge_requests(cls, merge_requests, wip, discord, slack, rocketchat):
"""Iterate through each merge request of a repo
and build the message array.
"""
final_message = ''
message_array = []
discord_message_array = []
for merge_request in merge_requests:
# TODO: There is a "work_in_progress" key in the response
# that could be used? https://docs.gitlab.com/ee/api/merge_requests.html
if not wip and 'WIP' in merge_request['title'].upper():
continue
else:
message = cls.prepare_message(merge_request)
final_message += message
return final_message

@classmethod
def prepare_message(cls, merge_request):
"""Prepare the message with merge request data.
"""
try:
if merge_request['assignees'][0]['username']:
users = ''
for assignee in merge_request['assignees']:
user = f"<{assignee['web_url']}|{assignee['username']}>"
users += user + ' '
else:
users = 'No assignee'
except IndexError:
users = 'No assignee'

# Truncate description after 120 characters
description = (merge_request['description'][:120] +
'...') if len(merge_request['description']) > 120 else merge_request['description']
message = f"\n:arrow_heading_up: *Merge Request:* <{merge_request['web_url']}|" + \
f"{merge_request['title']}>\n*Description:* {description}\n*Waiting on:* {users}\n"

return message
message, discord_message = Messages.prepare_gitlab_message(merge_request, discord, slack, rocketchat)
message_array.append(message)
discord_message_array.append(discord_message)
return message_array, discord_message_array

0 comments on commit ab272f1

Please sign in to comment.