Skip to content

Conversation

@mvadari
Copy link
Collaborator

@mvadari mvadari commented Dec 23, 2025

This PR adds a bot that manages discussions in this repo. It comments in a discussion when it is old and closes and locks it if nobody responds.

Example: #97 (comment)
image

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an automated discussion management system that identifies stale discussions (inactive for over ~3 years), warns users about impending closure, and automatically closes/locks discussions that remain inactive for 30 days after the warning.

Key Changes:

  • Added a GitHub Actions workflow that runs weekly to process stale discussions
  • Implemented a bash script that uses GitHub's GraphQL API to identify, warn, and close inactive discussions
  • Configured authentication via GitHub App for API access

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.

File Description
.github/workflows/discussions.yml Defines the scheduled workflow with environment variables for stale thresholds, warning/close messages, and integrates with a GitHub App for authentication
.github/scripts/process-stale-discussions.sh Implements the core logic to fetch discussions via GraphQL, identify which need warnings or closure based on age, and execute the appropriate actions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

contents: read # Required to checkout the repository

env:
STALE_DAYS: 1000 # 365 * 3 # Number of days without updates to consider a discussion stale
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The STALE_DAYS is set to 1000 days (approximately 2.7 years) but the comment says "365 * 3" which equals 1095 days. There's a mismatch between the actual value (1000) and the comment (365 * 3 = 1095). Either update the value to 1095 or change the comment to reflect 1000 days.

Suggested change
STALE_DAYS: 1000 # 365 * 3 # Number of days without updates to consider a discussion stale
STALE_DAYS: 1000 # Number of days without updates (≈2.7 years) to consider a discussion stale

Copilot uses AI. Check for mistakes.
Comment on lines 37 to 67
gh api graphql -f query='
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
discussions(first: 100, after: $cursor, orderBy: {field: UPDATED_AT, direction: ASC}) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
title
url
createdAt
updatedAt
closed
locked
comments(last: 10) {
nodes {
body
createdAt
author {
login
}
}
}
}
}
}
}
' -f owner="$GITHUB_REPOSITORY_OWNER" -f repo="$GITHUB_REPOSITORY_NAME" > discussions.json
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GraphQL query only fetches the first 100 discussions and doesn't implement pagination despite having pageInfo.hasNextPage and endCursor fields. If the repository has more than 100 discussions, only the first 100 will be processed. Implement pagination to process all discussions in the repository.

Copilot uses AI. Check for mistakes.
# 3. The discussion hasn't been updated since the warning (or updates are also old)
echo ""
echo "=== Discussions to close - warned ${WARNING_DAYS}+ days ago with no activity ==="
cat discussions.json | jq -r --arg warningCutoff "$CLOSE_CUTOFF" '.data.repository.discussions.nodes[] | select(.closed == false) | . as $discussion | ((.comments.nodes // []) | map(select(.body | contains("will be closed in 30 days"))) | last) as $warningComment | select($warningComment != null) | select($warningComment.createdAt < $warningCutoff) | select($discussion.updatedAt <= $warningComment.createdAt or $discussion.updatedAt < $warningCutoff) | @json' | while IFS= read -r discussion; do
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition 'select($discussion.updatedAt <= $warningComment.createdAt or $discussion.updatedAt < $warningCutoff)' on line 76 has a logic issue. The second part '$discussion.updatedAt < $warningCutoff' is redundant because if the warning comment was created before CLOSE_CUTOFF and the discussion hasn't been updated since the warning, then updatedAt would already be less than warningCutoff. This could also incorrectly close discussions where someone replied after the warning but the reply is still older than the close cutoff. Consider simplifying to just check if the discussion hasn't been updated since the warning comment.

Suggested change
cat discussions.json | jq -r --arg warningCutoff "$CLOSE_CUTOFF" '.data.repository.discussions.nodes[] | select(.closed == false) | . as $discussion | ((.comments.nodes // []) | map(select(.body | contains("will be closed in 30 days"))) | last) as $warningComment | select($warningComment != null) | select($warningComment.createdAt < $warningCutoff) | select($discussion.updatedAt <= $warningComment.createdAt or $discussion.updatedAt < $warningCutoff) | @json' | while IFS= read -r discussion; do
cat discussions.json | jq -r --arg warningCutoff "$CLOSE_CUTOFF" '.data.repository.discussions.nodes[] | select(.closed == false) | . as $discussion | ((.comments.nodes // []) | map(select(.body | contains("will be closed in 30 days"))) | last) as $warningComment | select($warningComment != null) | select($warningComment.createdAt < $warningCutoff) | select($discussion.updatedAt <= $warningComment.createdAt) | @json' | while IFS= read -r discussion; do

Copilot uses AI. Check for mistakes.
name: Stale GitHub Discussions

on:
pull_request: # just for debug purposes
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow trigger includes "pull_request" with a comment indicating it's "just for debug purposes". Debug triggers should not be included in production code. Remove this trigger before merging to production, or use workflow_dispatch for manual testing instead.

Suggested change
pull_request: # just for debug purposes

Copilot uses AI. Check for mistakes.
# b. It has a warning but was updated after that warning (user responded, so we warn again)
echo ""
echo "=== Discussions to warn - stale for ${STALE_DAYS}+ days, not yet warned ==="
cat discussions.json | jq -r --arg staleCutoff "$STALE_CUTOFF" '.data.repository.discussions.nodes[] | select(.closed == false) | select(.updatedAt < $staleCutoff) | . as $discussion | ((.comments.nodes // []) | map(select(.body | contains("will be closed in 30 days"))) | last) as $warningComment | select($warningComment == null or $discussion.updatedAt > $warningComment.createdAt) | @json' | while IFS= read -r discussion; do
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same hardcoded string "will be closed in 30 days" is used here for detecting warning comments. This creates a tight coupling with the WARNING_MESSAGE content and will break if WARNING_DAYS or the message format changes. Consider using a unique marker or tag in the warning message that doesn't depend on the specific day count.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +102
gh api graphql -f query='mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { comment { id } } }' -f discussionId="$DISCUSSION_ID" -f body="$CLOSE_MESSAGE"

# Step 2: Close the discussion
echo " Closing discussion..."
gh api graphql -f query='mutation($discussionId: ID!) { closeDiscussion(input: {discussionId: $discussionId}) { discussion { id } } }' -f discussionId="$DISCUSSION_ID"

# Step 3: Lock the discussion to prevent further comments
echo " Locking discussion..."
gh api graphql -f query='mutation($discussionId: ID!) { lockLockable(input: {lockableId: $discussionId}) { lockedRecord { locked } } }' -f discussionId="$DISCUSSION_ID"
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling could be improved here. If the gh api graphql mutation fails (for adding a comment, closing, or locking), the script will exit due to 'set -e' on line 2, but there's no cleanup or rollback logic. Consider adding error handling to ensure partial operations don't leave discussions in an inconsistent state (e.g., commented but not closed).

Suggested change
gh api graphql -f query='mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { comment { id } } }' -f discussionId="$DISCUSSION_ID" -f body="$CLOSE_MESSAGE"
# Step 2: Close the discussion
echo " Closing discussion..."
gh api graphql -f query='mutation($discussionId: ID!) { closeDiscussion(input: {discussionId: $discussionId}) { discussion { id } } }' -f discussionId="$DISCUSSION_ID"
# Step 3: Lock the discussion to prevent further comments
echo " Locking discussion..."
gh api graphql -f query='mutation($discussionId: ID!) { lockLockable(input: {lockableId: $discussionId}) { lockedRecord { locked } } }' -f discussionId="$DISCUSSION_ID"
if ! gh api graphql -f query='mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { comment { id } } }' -f discussionId="$DISCUSSION_ID" -f body="$CLOSE_MESSAGE"; then
echo " Error: Failed to add close comment for discussion #$DISCUSSION_NUMBER. Skipping close/lock for this discussion."
echo ""
continue
fi
# Step 2: Close the discussion
echo " Closing discussion..."
if ! gh api graphql -f query='mutation($discussionId: ID!) { closeDiscussion(input: {discussionId: $discussionId}) { discussion { id } } }' -f discussionId="$DISCUSSION_ID"; then
echo " Error: Failed to close discussion #$DISCUSSION_NUMBER after adding close comment. Skipping lock for this discussion."
echo ""
continue
fi
# Step 3: Lock the discussion to prevent further comments
echo " Locking discussion..."
if ! gh api graphql -f query='mutation($discussionId: ID!) { lockLockable(input: {lockableId: $discussionId}) { lockedRecord { locked } } }' -f discussionId="$DISCUSSION_ID"; then
echo " Warning: Failed to lock discussion #$DISCUSSION_NUMBER after closing it. Discussion remains closed but unlocked."
fi

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants