DEV Community

Cover image for Solved: Remind Developers of Stale Pull Requests via Slack Bot
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Remind Developers of Stale Pull Requests via Slack Bot

🚀 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 requests library 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 repo scope and Slack Webhook URLs, loaded from a secrets.txt file using python-dotenv to prevent hardcoding sensitive information.
  • Stale pull requests are identified by comparing their updated\_at timestamp against a configurable STALE\_DAYS threshold, 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 repo scope 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.

  1. Go to api.slack.com/apps and click “Create New App”.
  2. Choose “From scratch”, give your app a name (e.g., “PR Reminder Bot”), and select your Slack workspace.
  3. Once created, navigate to “Features” > “Incoming Webhooks” in the left sidebar.
  4. Toggle “Activate Incoming Webhooks” to “On”.
  5. 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.
  6. Copy the “Webhook URL”. This URL is crucial for sending messages to Slack. Keep it secure.
  7. Next, go to “Features” > “OAuth & Permissions” in the left sidebar.
  8. Under “Scopes” > “Bot Token Scopes”, add the chat:write permission.
  9. Scroll up and click “Install to Workspace” (or “Reinstall App” if you’ve added new scopes).
  10. 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"
Enter fullscreen mode Exit fullscreen mode

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.

  1. Go to your GitHub profile settings. Click on your profile picture > “Settings”.
  2. In the left sidebar, scroll down and click “Developer settings”.
  3. Click “Personal access tokens” > “Tokens (classic)”.
  4. Click “Generate new token” > “Generate new token (classic)”.
  5. Give your token a descriptive name (e.g., “PR Reminder Bot Token”).
  6. Set an expiration date if desired (recommended for security).
  7. Under “Select scopes”, check repo (this grants full control of private repositories, which includes reading PRs). For read-only access, you might only need public_repo for public repos or more granular scopes if available.
  8. Click “Generate token”.
  9. 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Explanation of the code:

  • load_dotenv(dotenv_path='secrets.txt'): This line loads environment variables from our secrets.txt file, making tokens accessible via os.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 Authorization header 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_at timestamp. 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.
  • main function:
    • Calls get_stale_pull_requests to 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.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. 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.
  2. Incorrect API Tokens/Permissions: Ensure your GitHub PAT has the correct repo scope 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.
  3. 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.
  4. secrets.txt not loaded: If your script fails to fetch tokens, ensure secrets.txt is in the same directory as your script, or provide the correct path to load_dotenv. Also, ensure the format within secrets.txt is 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!


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)