Automate Locking Closed Issues and Pull Requests on GitHub¶
Locking a closed thread prevents new comments from being added. It keeps old threads quiet and lets maintainers direct their attention toward work that is still active. This post covers two approaches: a shell script using the GitHub CLI for one-time or ad hoc locking across one or more repositories, and a GitHub Actions workflow using dessant/lock-threads for ongoing automation.
Why Lock Closed Threads¶
When an issue or pull request is closed, the conversation is usually finished. The bug is fixed, the feature is shipped, the decision is made. But the thread stays open for comments by default, and it tends to attract a particular kind of follow-up.
The most common patterns are:
- Status requests on resolved issues. Someone discovers a closed bug report months after the fix shipped, posts "is this resolved?", and generates a notification for every subscriber. The answer is in the issue title: it is closed.
- "Same here" comments on closed issues. A contributor runs into a similar problem, finds the closed issue, and posts to the thread rather than opening a new one. The maintainer sees the notification, reads the comment, and has to decide whether the new report is the same issue or a related one. It is rarely the same issue.
- Requests to reopen. Not all of these are unreasonable, but most closed threads that receive reopen requests have been closed intentionally. The request does not provide new information; it just generates overhead.
- Off-topic activity. Old, high-visibility threads (particularly ones that appeared in search results for a popular error message) sometimes become drop boxes for tangentially related questions. None of this benefits the project.
Each of these generates a notification. Notifications cost attention. On a project with hundreds or thousands of closed threads, the cumulative drag is real.
Locking threads after they have been closed for a defined period draws a clear boundary. Old discussions are preserved and readable; they just cannot receive new replies. Contributors who encounter a locked thread are prompted to open a new issue if they have a related but distinct problem, which is almost always the right outcome.
The Script¶
For a one-time cleanup or ad hoc use, a shell script using the GitHub CLI handles bulk locking directly from the command line.
Prerequisites¶
You need two tools installed and available on your PATH.
Install the GitHub CLI¶
After installation, authenticate with GitHub:
Run gh auth status to confirm your session is active before continuing.
Install jq¶
The Script¶
Test Before Running at Scale
Before running this script against any production repository, test it on a private repository you control with a small number of closed issues and pull requests. Locking a thread is reversible (you can unlock it via the UI or API), but running the script at scale against the wrong repository is easy to do and harder to recover from.
This script is provided as-is with no warranties and confers no rights. You are responsible for validating it in your own environment before use.
#!/bin/bash
# Array of repositories in the format "owner/repo"
repos=(
"owner/foo"
"owner/bar"
"owner/baz"
)
# Function to check if issues are enabled in a repository
issues_enabled() {
local repo=$1
gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo" | jq -r '.has_issues'
}
# Function to check if an issue is locked
is_issue_locked() {
local repo=$1
local issue_number=$2
gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo/issues/$issue_number" | jq -r '.locked'
}
# Function to check if a pull request is locked
is_pr_locked() {
local repo=$1
local pr_number=$2
gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo/pulls/$pr_number" | jq -r '.locked'
}
# Function to lock all closed issues and pull requests in a repository
lock_closed_issues_and_prs() {
local repo=$1
echo "Locking closed issues and pull requests in $repo..."
if [ "$(issues_enabled "$repo")" == "true" ]; then
# Lock all closed issues
closed_issues=$(gh issue list -R "$repo" --state closed --json number --jq '.[].number')
for issue in $closed_issues; do
if [ "$(is_issue_locked "$repo" "$issue")" == "false" ]; then
gh issue lock "$issue" -R "$repo" --reason resolved
echo "Locked closed issue #$issue in $repo"
fi
done
else
echo "Issues are not enabled in $repo"
fi
# Lock all closed pull requests
closed_prs=$(gh pr list -R "$repo" --state closed --json number --jq '.[].number')
for pr in $closed_prs; do
if [ "$(is_pr_locked "$repo" "$pr")" == "false" ]; then
gh pr lock "$pr" -R "$repo" --reason resolved
echo "Locked closed pull request #$pr in $repo"
fi
done
}
# Iterate over each repository and lock closed issues and pull requests
for repo in "${repos[@]}"; do
lock_closed_issues_and_prs "$repo"
done
echo "All closed issues and pull requests have been locked in the specified repositories."
Set each entry in the repos array to the full owner/repo path for every repository you want to process. The script skips issues in repositories where the Issues feature is disabled and skips any thread that is already locked, so it is safe to run more than once.
How It Works¶
- For each repository in the
reposarray, the script callsissues_enabledto confirm whether the Issues feature is active on that repository before attempting to list issues. gh issue listandgh pr listretrieve all closed issues and pull requests, returning only the number field as a list.- For each number, the script checks the current lock status. If the thread is already locked, it is skipped.
gh issue lockandgh pr locklock the thread with theresolvedreason, which is the most appropriate reason for closed items.
Targeting Multiple Repositories¶
To run the script across several repositories at once, add each one to the repos array:
The script iterates over the full list, so adding repositories is the only change needed to extend its scope.
Tip
The same pattern used here (iterating through gh CLI results and applying a bulk operation) also applies to other housekeeping tasks. The post on bulk-deleting GitHub Actions workflow runs covers a similar approach for clearing accumulated run history.
Automating with GitHub Actions¶
The script handles one-time or ad hoc runs well. For repositories that are actively receiving closed issues and pull requests on an ongoing basis, automating the locking process is more practical. The dessant/lock-threads action runs on a schedule and locks threads that have been closed for a configurable number of days.
The Workflow¶
---
name: Lock
on:
schedule:
- cron: "30 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
lock:
name: Lock Threads
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
discussions: write
steps:
- name: Lock Threads
uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-comment: >-
I'm going to lock this issue because it has been closed for 30
days. This helps our maintainers find and focus on the active
issues.
If you have found a problem that seems similar to this,
please open a new issue and complete the issue template so we can
capture all the details necessary to investigate further.
issue-inactive-days: "30"
pr-comment: >-
I'm going to lock this pull request because it has been closed for
30 days. This helps our maintainers find and focus on the active
issues.
If you have found a problem that seems related to this change,
please open a new issue and complete the issue template so we can
capture all the details necessary to investigate further.
pr-inactive-days: "30"
Save this file to .github/workflows/lock.yml in your repository. The workflow runs daily at 00:30 UTC and can also be triggered manually from the Actions tab using workflow_dispatch.
Understanding the Configuration¶
issue-inactive-days and pr-inactive-days control how long a closed thread must have been inactive before it is locked. The example above uses "30", so any issue or pull request closed for 30 or more days without recent activity is a candidate for locking.
issue-comment and pr-comment set the message posted to the thread before it is locked. The comment communicates the reason for locking and directs contributors toward the right path if they have a related problem. The >- syntax in YAML produces a single folded string; blank lines within the block become paragraph breaks in the rendered comment.
Using the GITHUB_TOKEN for authentication scopes the action to the repository where the workflow runs. No additional secrets or tokens are required.
Adding Discussion Support¶
The lock-threads action supports GitHub Discussions in addition to issues and pull requests. To include discussions in the locking policy, add the discussion-specific inputs alongside the issue and pull request ones:
- name: Lock Threads
uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: "30"
pr-inactive-days: "30"
discussion-inactive-days: "30"
discussion-comment: >-
I'm going to lock this discussion because it has been closed for
30 days. This helps our maintainers find and focus on the active
discussions.
If you have a related question or a new topic to raise, please
open a new discussion.
The discussions: write permission in the job's permissions block is already present in the workflow above, so no change to the permissions configuration is needed to enable this.
Choosing Between the Two Approaches¶
The script and the action address different situations.
Use the Script when you need to lock a large backlog of already-closed threads in one pass. If a repository has been active for years without a locking policy, there may be hundreds or thousands of closed threads that have never been locked. Running the script once clears that backlog. It also works well for locking across a specific list of repositories in one operation, without needing to add the workflow file to each one individually.
Use the Action when you want ongoing automation. Once the workflow is committed, newly closed threads are locked automatically after the configured inactivity period. There is nothing to run manually, and the policy is version-controlled alongside the rest of the repository.
For a repository that has been around for a while and is transitioning to a locking policy, the most practical approach is to do both: run the script once to lock the existing backlog, then add the workflow to handle everything going forward.