đ Executive Summary
TL;DR: This guide details building a Python Slack bot to automatically remind developers about stale pull requests, addressing the inefficiency of manual tracking. By integrating with GitHubâs API and Slack webhooks, the bot ensures a healthy code review pipeline and reduces context switching for development teams.
đŻ Key Takeaways
- The solution leverages Pythonâs
requestslibrary to interact with the GitHub API for fetching pull request data and Slack Incoming Webhooks for sending notifications. - Secure authentication is managed through GitHub Personal Access Tokens (PAT) with
reposcope and Slack Webhook URLs, loaded from asecrets.txtfile usingpython-dotenvto prevent hardcoding sensitive information. - Stale pull requests are identified by comparing their
updated\_attimestamp against a configurableSTALE\_DAYSthreshold, with options to exclude draft PRs or those marked with specific labels like âWIPâ or âblockedâ.
Remind Developers of Stale Pull Requests via Slack Bot
Introduction: Reclaiming Focus from Stale Pull Requests
In the fast-paced world of software development, code reviews are paramount to maintaining quality and fostering collaboration. However, a common pain point for many development teams is the accumulation of âstaleâ pull requests (PRs) â those that sit unreviewed, unmerged, or unattended for days, sometimes weeks. This not only clogs up your teamâs workflow but also leads to context switching, missed deadlines, and overall frustration. Manually chasing down developers or reviewing aging PR lists is tedious, inefficient, and frankly, a waste of valuable engineering time.
What if you could automate this mundane but critical task? Imagine a dedicated assistant that quietly monitors your repositories and gently reminds developers about their pending PRs, directly in their preferred communication channel. This tutorial will guide you through building a simple yet powerful Slack bot using Python that does exactly that. By the end, youâll have a robust solution to keep your code review pipeline flowing smoothly, allowing your team to focus on what they do best: building amazing software.
Prerequisites
Before we dive into the implementation, ensure you have the following tools and accounts ready:
- Python 3.x: The script will be written in Python. Python 3.6 or newer is recommended.
- Pip: Pythonâs package installer, usually included with Python 3.x.
- Git Platform Account: Access to a Git hosting service like GitHub or GitLab, with a repository containing pull requests. For this tutorial, we will focus on GitHub.
-
GitHub Personal Access Token (PAT): A token with
reposcope to allow your script to read repository information and pull requests. - Slack Workspace: An active Slack workspace where your development team communicates.
- Slack App with Bot Token and Webhook: Weâll create a Slack application, enable incoming webhooks, and generate a bot user OAuth token.
- Basic understanding of APIs: Familiarity with RESTful APIs and JSON data structures will be beneficial.
Step-by-Step Guide: Building Your Stale PR Notifier
Step 1: Set up Your Slack Application
First, we need to create a Slack application that will enable our bot to send messages.
- Go to api.slack.com/apps and click âCreate New Appâ.
- Choose âFrom scratchâ, give your app a name (e.g., âPR Reminder Botâ), and select your Slack workspace.
- Once created, navigate to âFeaturesâ > âIncoming Webhooksâ in the left sidebar.
- Toggle âActivate Incoming Webhooksâ to âOnâ.
- Scroll down and click âAdd New Webhook to Workspaceâ. Choose the default channel where the bot will post (you can override this in the script later) and authorize.
- Copy the âWebhook URLâ. This URL is crucial for sending messages to Slack. Keep it secure.
- Next, go to âFeaturesâ > âOAuth & Permissionsâ in the left sidebar.
- Under âScopesâ > âBot Token Scopesâ, add the
chat:writepermission. - Scroll up and click âInstall to Workspaceâ (or âReinstall Appâ if youâve added new scopes).
- Copy the âBot User OAuth Tokenâ (starts with
xoxb-). This token is needed if you decide to use the Slack API directly for more advanced features like listing users or conversations, but for simple messages, the webhook is sufficient. For this tutorial, we will primarily use the Webhook URL for simplicity in sending messages, but the bot token could be used for more interactive features.
Create a file named secrets.txt (remembering our security rules) to store your Slack Webhook URL. This file should be outside your version control system.
SLACK_WEBHOOK_URL="YOUR_SLACK_WEBHOOK_URL_HERE"
Step 2: Generate a GitHub Personal Access Token (PAT)
Our script needs to authenticate with GitHub to fetch pull request data. A Personal Access Token is the most secure way to do this for automation.
- Go to your GitHub profile settings. Click on your profile picture > âSettingsâ.
- In the left sidebar, scroll down and click âDeveloper settingsâ.
- Click âPersonal access tokensâ > âTokens (classic)â.
- Click âGenerate new tokenâ > âGenerate new token (classic)â.
- Give your token a descriptive name (e.g., âPR Reminder Bot Tokenâ).
- Set an expiration date if desired (recommended for security).
- Under âSelect scopesâ, check
repo(this grants full control of private repositories, which includes reading PRs). For read-only access, you might only needpublic_repofor public repos or more granular scopes if available. - Click âGenerate tokenâ.
- IMPORTANT: Copy the token immediately. You will not be able to see it again.
Add your GitHub PAT to the same secrets.txt file:
SLACK_WEBHOOK_URL="YOUR_SLACK_WEBHOOK_URL_HERE"
GITHUB_TOKEN="YOUR_GITHUB_PERSONAL_ACCESS_TOKEN_HERE"
Step 3: Develop the Python Script
Now, letâs write the Python script that orchestrates the entire process. Weâll use the requests library to interact with both the GitHub API and the Slack Webhook.
First, install the necessary library:
pip install requests python-dotenv
Create a Python file, for example, pr_reminder_bot.py.
import os
import requests
from datetime import datetime, timedelta
from dotenv import load_dotenv
# Load environment variables from secrets.txt
# This ensures sensitive information is not hardcoded
load_dotenv(dotenv_path='secrets.txt')
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
# --- Configuration ---
# GitHub organization/owner and repository
GITHUB_OWNER = "your-github-org-or-username" # e.g., "TechResolve"
GITHUB_REPO = "your-repository-name" # e.g., "awesome-project"
# You can list multiple repositories as a list of dictionaries if needed
# GITHUB_REPOS = [
# {"owner": "TechResolve", "repo": "project-alpha"},
# {"owner": "TechResolve", "repo": "project-beta"}
# ]
STALE_DAYS = 5 # Number of days after which a PR is considered stale
EXCLUDE_LABELS = ["do not merge", "WIP", "blocked"] # Labels to exclude from reminders
TARGET_SLACK_CHANNEL = "#dev-reminders" # Override webhook default channel if specified
# --- GitHub API interaction ---
def get_stale_pull_requests(owner, repo, stale_days, exclude_labels):
url = f"https://api.github.com/repos/{owner}/{repo}/pulls"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
try:
response = requests.get(url, headers=headers, params={"state": "open"})
response.raise_for_status() # Raise an exception for HTTP errors
pull_requests = response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching PRs from GitHub: {e}")
return []
stale_prs = []
now = datetime.utcnow() # Use UTC for consistent date comparisons
for pr in pull_requests:
# Skip draft PRs
if pr.get("draft"):
continue
# Check for excluded labels
pr_labels = [label["name"].lower() for label in pr.get("labels", [])]
if any(label in pr_labels for label in exclude_labels):
continue
updated_at = datetime.strptime(pr["updated_at"], "%Y-%m-%dT%H:%M:%SZ")
age_days = (now - updated_at).days
if age_days >= stale_days:
stale_prs.append({
"title": pr["title"],
"url": pr["html_url"],
"author": pr["user"]["login"],
"age": age_days,
"reviewers": [reviewer["login"] for reviewer in pr.get("requested_reviewers", [])]
})
return stale_prs
# --- Slack API interaction ---
def send_slack_message(message_text, channel=None):
if not SLACK_WEBHOOK_URL:
print("SLACK_WEBHOOK_URL is not set. Cannot send message.")
return
payload = {
"text": message_text
}
if channel:
payload["channel"] = channel # Override default channel
try:
response = requests.post(SLACK_WEBHOOK_URL, json=payload)
response.raise_for_status()
print("Slack message sent successfully.")
except requests.exceptions.RequestException as e:
print(f"Error sending message to Slack: {e}")
# --- Main execution logic ---
def main():
print(f"Checking for stale PRs in {GITHUB_OWNER}/{GITHUB_REPO} older than {STALE_DAYS} days...")
stale_prs = get_stale_pull_requests(GITHUB_OWNER, GITHUB_REPO, STALE_DAYS, EXCLUDE_LABELS)
if not stale_prs:
print("No stale pull requests found. Good job, team!")
send_slack_message(f"> No stale pull requests found in *{GITHUB_OWNER}/{GITHUB_REPO}* older than {STALE_DAYS} days. Keep up the great work! :tada:", TARGET_SLACK_CHANNEL)
return
message_blocks = []
message_blocks.append(f"Hey team! :wave: The following pull requests in *{GITHUB_OWNER}/{GITHUB_REPO}* are looking a bit lonely ({STALE_DAYS}+ days old). Could you give them some love?\n\n")
for pr in stale_prs:
reviewers_str = ""
if pr["reviewers"]:
reviewers_str = f" (Reviewers: {', '.join(['<@' + r + '>' for r in pr['reviewers']])})"
message_blocks.append(
f"> *<{pr['url']}|{pr['title']}>* by <@{pr['author']}>"
f" - Last updated *{pr['age']}* days ago. {reviewers_str}\n"
)
# Slack limits block kit to 50 items. For simple text, concatenating is fine.
# For more advanced Slack formatting, consider using Slack's Block Kit Builder.
full_message = "".join(message_blocks)
send_slack_message(full_message, TARGET_SLACK_CHANNEL)
if __name__ == "__main__":
main()
Explanation of the code:
-
load_dotenv(dotenv_path='secrets.txt'): This line loads environment variables from oursecrets.txtfile, making tokens accessible viaos.getenv(). - Configuration: We define constants for your GitHub owner/repo, the threshold for âstaleâ days, labels to exclude (e.g., âWIPâ), and the Slack channel for notifications.
-
get_stale_pull_requests:- Constructs the GitHub API URL for fetching pull requests.
- Includes the
Authorizationheader with your GitHub token. - Filters for âopenâ PRs.
- Iterates through each PR, checking if itâs a draft or has excluded labels.
- Calculates the age of the PR based on its
updated_attimestamp. We use UTC to avoid timezone issues. - Appends relevant PR details (title, URL, author, age, reviewers) to a list if it meets the âstaleâ criteria.
-
send_slack_message:- Takes a message string and an optional channel name.
- Constructs a JSON payload for the Slack Incoming Webhook.
- Posts the payload to the
SLACK_WEBHOOK_URL. - Includes basic error handling for network issues.
-
mainfunction:- Calls
get_stale_pull_requeststo retrieve the data. - If no stale PRs are found, it sends a celebratory message to Slack.
- If stale PRs exist, it constructs a formatted message, including direct links to PRs and mentioning reviewers using Slackâs
<@username>syntax (note: this requires the Slack usernames to match GitHub usernames for direct mentions to work, or you might need a mapping). - Sends the aggregated message to Slack.
- Calls
Remember to replace placeholders like your-github-org-or-username and your-repository-name with your actual values.
Step 4: Automate the Script Execution
To make this truly useful, the script needs to run periodically. Weâll use cron for Linux/macOS systems.
Open your userâs cron table:
crontab -e
Add a line similar to this to run the script every weekday morning at 9:00 AM (e.g., Monday-Friday):
0 9 * * 1-5 /usr/bin/python3 /path/to/your/pr_reminder_bot.py >> /tmp/logs/pr_bot.log 2>&1
Explanation of the cron job:
-
0 9 * * 1-5: This is the cron schedule. It means âat minute 0, hour 9, every day of the month, every month, only on days 1 through 5 (Monday through Friday)â. -
/usr/bin/python3: This should be the absolute path to your Python 3 interpreter. You can find it by runningwhich python3. -
/path/to/your/pr_reminder_bot.py: The absolute path to your Python script. -
>> /tmp/logs/pr_bot.log 2>&1: This redirects standard output and standard error to a log file. Itâs crucial for debugging cron jobs, as they donât have a visible console. Ensure the/tmp/logs/directory exists.
Save and exit the cron editor. Your bot will now automatically check for and remind about stale PRs!
For more advanced deployment, consider encapsulating your application in a Docker container and scheduling it with tools like Kubernetes cron jobs or a cloud-based scheduler (e.g., AWS EventBridge + Lambda, Google Cloud Scheduler + Cloud Run).
Common Pitfalls
- API Rate Limits: GitHub has rate limits (e.g., 5000 requests per hour for authenticated users). For a single repository or a few, this script is unlikely to hit them. However, if you expand to many repositories or a very large organization, monitor your API usage. If you hit limits, consider caching results or introducing delays.
-
Incorrect API Tokens/Permissions: Ensure your GitHub PAT has the correct
reposcope and your Slack Webhook URL is accurate. A common mistake is using the wrong token type (e.g., trying to use a Slack bot token as an incoming webhook URL). Double-check the permissions granted to your GitHub PAT and Slack App. If the bot isnât posting to a private channel, ensure the bot has been invited to that channel. -
Timezone Discrepancies: When calculating PR age, using UTC (
datetime.utcnow()) for comparisons is vital to prevent issues arising from local time differences or Daylight Saving Time. The GitHub API typically returns timestamps in UTC. -
secrets.txtnot loaded: If your script fails to fetch tokens, ensuresecrets.txtis in the same directory as your script, or provide the correct path toload_dotenv. Also, ensure the format withinsecrets.txtis correct (KEY="VALUE").
Conclusion
Youâve successfully built and deployed an automated Slack bot to remind your development team about stale pull requests! This small automation significantly reduces manual overhead, keeps your PR queue healthy, and promotes a more efficient code review culture. By leveraging Python, GitHub, and Slack APIs, youâve transformed a tedious task into a seamless, automated process.
Whatâs next? Consider enhancing your bot:
- More Granular Notifications: Implement custom rules for different teams or repositories.
- Interactive Reminders: Use Slackâs Block Kit to create more visually appealing and interactive messages, perhaps with buttons to âMark as reviewedâ or âClose PRâ.
-
Team/User Mapping: Create a mapping file to link GitHub usernames to Slack user IDs for direct
<@user_id>mentions, ensuring reminders go directly to the right person. - Advanced Filtering: Filter by specific labels (e.g., âneeds-reviewâ), number of comments, or target branch.
- Dry Run Mode: Add an option to run the script without sending Slack messages, useful for testing.
- Error Reporting: Set up error notifications for yourself if the script encounters an issue.
Happy automating!
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)