-
Notifications
You must be signed in to change notification settings - Fork 1k
Discussion management #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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 |
| 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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| # 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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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 |
| name: Stale GitHub Discussions | ||
|
|
||
| on: | ||
| pull_request: # just for debug purposes |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| pull_request: # just for debug purposes |
| # 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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| 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" |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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).
| 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 |
Co-authored-by: Copilot <[email protected]>
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)
