🚀 Executive Summary

TL;DR: This guide provides a Python script and cron job setup to automatically identify, comment on, and close stale GitHub issues after 30 days of inactivity. This automation helps maintain a clean and relevant issue tracker, reducing manual backlog management and improving project focus.

🎯 Key Takeaways

  • A GitHub Personal Access Token (PAT) with ‘repo’ scope is required for the Python script to interact with GitHub’s API, and it should be secured using environment variables via `python-dotenv`.
  • The Python script leverages the `PyGithub` library to fetch open issues, calculates a stale threshold using `datetime` and `timedelta`, and then uses `issue.create_comment()` and `issue.edit(state=’closed’)` to manage stale issues.
  • Scheduling the script with a cron job (e.g., `0 2 * * 1 python3 script.py` for Monday 2 AM) automates the process, ensuring regular cleanup of the repository backlog.

Auto-Close Stale Issues on GitHub after 30 days

Auto-Close Stale Issues on GitHub after 30 days

Hey team, Darian Vance here. Let’s talk about technical debt’s quiet cousin: backlog clutter. I used to spend my Monday mornings manually poking through stale GitHub issues, trying to decide what was still relevant. It was a massive time sink. After setting up the simple automation I’m about to show you, I got those hours back, and our issue tracker became a true reflection of our active work. It’s a small change that makes a big difference in keeping our projects focused.

This guide will walk you through setting up a Python script to automatically comment on and close GitHub issues that have been inactive for 30 days. Let’s get that backlog clean.

Prerequisites

Before we start, make sure you have the following ready:

  • A GitHub account with access to the target repository.
  • Python 3 installed on the system where you’ll run the script.
  • Administrative access on that system to schedule a recurring task (like a cron job).

The Step-by-Step Guide

Step 1: Get a GitHub Personal Access Token (PAT)

First things first, our script needs permission to interact with GitHub on your behalf. We’ll use a Personal Access Token for this.

  1. Navigate to your GitHub Settings > Developer settings > Personal access tokens > Tokens (classic).
  2. Click “Generate new token” and select “Generate new token (classic)”.
  3. Give it a descriptive name, like “Stale Issue Closer”.
  4. Set the expiration. I recommend 90 days or setting up a fine-grained token for better security, but for this classic token, a custom date is fine.
  5. Under Scopes, check the box for repo. This grants the token full control of private repositories, which includes reading issues and closing them.
  6. Click “Generate token”. Copy the token immediately. You won’t be able to see it again.

Pro Tip: Treat this token like a password. Never, ever commit it directly into your code. We’ll use an environment variable in the next step to keep it secure.

Step 2: Setting Up Your Local Project

I’ll skip the standard virtualenv setup commands since you likely have your own workflow for that. Just make sure you’re in a clean project directory. You’ll need to install a couple of Python libraries using pip; specifically, you need `PyGithub` to talk to the API and `python-dotenv` to manage our secret token.

Next, create two files in your project directory:

  • script.py: This will hold our main Python logic.
  • config.env: This file will store our sensitive credentials securely, keeping them out of the main script.

Inside your config.env file, add the following lines, replacing the placeholder values with your own:


GITHUB_TOKEN="paste_your_github_pat_here"
REPO_NAME="your-username/your-repository-name"

Step 3: The Python Script

Now for the core logic. Open script.py and paste the following code. I’ve added comments to explain what each part does.


import os
from datetime import datetime, timedelta, timezone
from github import Github
from dotenv import load_dotenv

# --- Configuration ---
# Load environment variables from our config.env file
load_dotenv('config.env')

GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
REPO_NAME = os.getenv("REPO_NAME")
DAYS_UNTIL_STALE = 30
STALE_COMMENT = "This issue has been inactive for over 30 days and is being automatically closed. If you feel this is still relevant, please reopen it with a new comment."

def main():
    """
    Main function to find and close stale GitHub issues.
    """
    if not GITHUB_TOKEN or not REPO_NAME:
        print("Error: GITHUB_TOKEN and REPO_NAME must be set in your config.env file.")
        return

    try:
        # First, we authenticate to GitHub using our PAT
        g = Github(GITHUB_TOKEN)
        repo = g.get_repo(REPO_NAME)
        print(f"Successfully connected to repo: {repo.full_name}")

        # Get a list of all issues that are currently open
        open_issues = repo.get_issues(state='open')

        # Calculate the cutoff date. Any issue not updated before this is stale.
        # We use timezone.utc to ensure our comparison is timezone-aware.
        now = datetime.now(timezone.utc)
        stale_threshold = now - timedelta(days=DAYS_UNTIL_STALE)

        print(f"Checking for issues not updated since: {stale_threshold.isoformat()}")

        closed_count = 0
        for issue in open_issues:
            # The 'updated_at' timestamp from GitHub is already in UTC.
            # We check if the last update time is older than our threshold.
            if issue.updated_at < stale_threshold:
                print(f"Issue #{issue.number} ('{issue.title}') is stale. Last updated: {issue.updated_at}")
                
                # Add a polite comment explaining why the issue is being closed.
                issue.create_comment(STALE_COMMENT)
                
                # Finally, close the issue.
                issue.edit(state='closed')
                print(f"-> Closed issue #{issue.number}.")
                closed_count += 1

        print(f"\nProcess complete. Closed {closed_count} stale issue(s).")

    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    main()

Pro Tip: Before you let this script run wild, I strongly recommend doing a “dry run”. Comment out the two lines that create the comment and close the issue (`issue.create_comment(…)` and `issue.edit(…)`). Run the script manually first to see which issues it *would* close. This prevents any unwanted surprises.

Step 4: Scheduling with Cron

A script is only useful if it runs automatically. On a Linux or macOS system, a cron job is the perfect tool for this. We’ll set it to run once a week.

You’ll need to add a line to your system’s crontab. The command would look something like this, assuming your script is in your user’s project folder:


0 2 * * 1 python3 script.py

This entry breaks down as follows:

  • 0 2 * * 1: This means “At 02:00 (2 AM) on Monday.”
  • python3 script.py: The command to execute. Make sure you run this from the directory containing your script and `config.env` file, or use appropriate paths.

This will now run every Monday morning, cleaning up the backlog for you before the week even starts.

Common Pitfalls (Where I Usually Mess Up)

  • Incorrect PAT Scopes: The number one issue I see is the PAT not having the right permissions. If you get a 404 or permission denied error, double-check that the repo scope is enabled on your token.
  • Environment Variables Not Loading: The script will fail silently or complain about a missing token if the config.env file is named incorrectly or is not in the same directory where you run the script.
  • Wrong Repository Name: Make sure the REPO_NAME in your config.env is in the format `”owner/repo”`, for example, `”TechResolve/Project-Phoenix”`. A typo here will lead to a “Not Found” error.

Conclusion

And that’s it. With a simple Python script and a cron job, you’ve automated a tedious but important part of project hygiene. This system ensures your issue tracker remains a relevant, actionable list of tasks rather than a graveyard of forgotten ideas. It’s a small investment of time that pays off every single week. Happy automating!

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ What is the primary benefit of automating GitHub issue closure?

Automating GitHub issue closure significantly reduces ‘backlog clutter’ and ‘technical debt’ by ensuring the issue tracker reflects only active, relevant work, thereby saving development teams time spent on manual cleanup and improving project focus.

âť“ How does this Python script approach compare to native GitHub features or other alternatives?

This custom Python script offers fine-grained control over the stale issue logic and messaging, running on a self-hosted schedule via cron. While GitHub Actions offers a ‘stale’ action for similar functionality, this script provides a lightweight, external solution for users who prefer a Python-based approach or specific execution environments, directly replacing manual issue management.

âť“ What is a common pitfall when setting up the GitHub Personal Access Token (PAT) for this script?

A common pitfall is providing incorrect PAT scopes. If the token lacks the necessary ‘repo’ scope, the script will encounter permission denied errors (e.g., 404), preventing it from commenting on or closing issues. Always double-check that the ‘repo’ scope is enabled during PAT generation.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading