feat: add daily Discord recaps for issues and PRs (#9904)
This commit is contained in:
166
.github/workflows/daily-issues-recap.yml
vendored
Normal file
166
.github/workflows/daily-issues-recap.yml
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
name: Daily Issues Recap
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
|
||||||
|
- cron: "0 23 * * *"
|
||||||
|
workflow_dispatch: # Allow manual trigger for testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
daily-recap:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Generate daily issues recap
|
||||||
|
id: recap
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: |
|
||||||
|
{
|
||||||
|
"bash": {
|
||||||
|
"*": "deny",
|
||||||
|
"gh issue*": "allow",
|
||||||
|
"gh search*": "allow"
|
||||||
|
},
|
||||||
|
"webfetch": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"write": "deny"
|
||||||
|
}
|
||||||
|
run: |
|
||||||
|
# Get today's date range
|
||||||
|
TODAY=$(date -u +%Y-%m-%d)
|
||||||
|
|
||||||
|
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
|
||||||
|
|
||||||
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
|
STEP 1: Gather today's issues
|
||||||
|
Search for all issues created today (${TODAY}) using:
|
||||||
|
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||||
|
|
||||||
|
STEP 2: Analyze and categorize
|
||||||
|
For each issue created today, categorize it:
|
||||||
|
|
||||||
|
**Severity Assessment:**
|
||||||
|
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
|
||||||
|
- HIGH: Significant bugs affecting many users, important features broken
|
||||||
|
- MEDIUM: Bugs with workarounds, minor features broken
|
||||||
|
- LOW: Minor issues, cosmetic, nice-to-haves
|
||||||
|
|
||||||
|
**Activity Assessment:**
|
||||||
|
- Note issues with high comment counts or engagement
|
||||||
|
- Note issues from repeat reporters (check if author has filed before)
|
||||||
|
|
||||||
|
STEP 3: Cross-reference with existing issues
|
||||||
|
For issues that seem like feature requests or recurring bugs:
|
||||||
|
- Search for similar older issues to identify patterns
|
||||||
|
- Note if this is a frequently requested feature
|
||||||
|
- Identify any issues that are duplicates of long-standing requests
|
||||||
|
|
||||||
|
STEP 4: Generate the recap
|
||||||
|
Create a structured recap with these sections:
|
||||||
|
|
||||||
|
===DISCORD_START===
|
||||||
|
**Daily Issues Recap - ${TODAY}**
|
||||||
|
|
||||||
|
**Summary Stats**
|
||||||
|
- Total issues opened today: [count]
|
||||||
|
- By category: [bugs/features/questions]
|
||||||
|
|
||||||
|
**Critical/High Priority Issues**
|
||||||
|
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
|
||||||
|
|
||||||
|
**Most Active/Discussed**
|
||||||
|
[Issues with significant engagement or from active community members]
|
||||||
|
|
||||||
|
**Trending Topics**
|
||||||
|
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
|
||||||
|
|
||||||
|
**Duplicates & Related**
|
||||||
|
[Issues that relate to existing open issues]
|
||||||
|
===DISCORD_END===
|
||||||
|
|
||||||
|
STEP 5: Format for Discord
|
||||||
|
Format the recap as a Discord-compatible message:
|
||||||
|
- Use Discord markdown (**, __, etc.)
|
||||||
|
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
|
||||||
|
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
|
||||||
|
- Group related issues on single lines where possible
|
||||||
|
- Add emoji sparingly for critical items only
|
||||||
|
- HARD LIMIT: Keep under 1800 characters total
|
||||||
|
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
|
||||||
|
- Prioritize signal over completeness - only surface what matters
|
||||||
|
|
||||||
|
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
|
||||||
|
|
||||||
|
# Extract only the Discord message between markers
|
||||||
|
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
|
||||||
|
|
||||||
|
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Post to Discord
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
||||||
|
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
||||||
|
cat /tmp/recap.txt
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read the recap
|
||||||
|
RECAP_RAW=$(cat /tmp/recap.txt)
|
||||||
|
RECAP_LENGTH=${#RECAP_RAW}
|
||||||
|
|
||||||
|
echo "Recap length: ${RECAP_LENGTH} chars"
|
||||||
|
|
||||||
|
# Function to post a message to Discord
|
||||||
|
post_to_discord() {
|
||||||
|
local msg="$1"
|
||||||
|
local content=$(echo "$msg" | jq -Rs '.')
|
||||||
|
curl -s -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d "{\"content\": ${content}}" \
|
||||||
|
"$DISCORD_WEBHOOK_URL"
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If under limit, send as single message
|
||||||
|
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
||||||
|
post_to_discord "$RECAP_RAW"
|
||||||
|
else
|
||||||
|
echo "Splitting into multiple messages..."
|
||||||
|
remaining="$RECAP_RAW"
|
||||||
|
while [ ${#remaining} -gt 0 ]; do
|
||||||
|
if [ ${#remaining} -le 1950 ]; then
|
||||||
|
post_to_discord "$remaining"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
||||||
|
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
||||||
|
chunk="${remaining:0:$last_newline}"
|
||||||
|
remaining="${remaining:$((last_newline+1))}"
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
remaining="${remaining:1900}"
|
||||||
|
fi
|
||||||
|
post_to_discord "$chunk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Posted daily recap to Discord"
|
||||||
174
.github/workflows/daily-pr-recap.yml
vendored
Normal file
174
.github/workflows/daily-pr-recap.yml
vendored
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
name: Daily PR Recap
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
|
||||||
|
- cron: "0 22 * * *"
|
||||||
|
workflow_dispatch: # Allow manual trigger for testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-recap:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Generate daily PR recap
|
||||||
|
id: recap
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENCODE_PERMISSION: |
|
||||||
|
{
|
||||||
|
"bash": {
|
||||||
|
"*": "deny",
|
||||||
|
"gh pr*": "allow",
|
||||||
|
"gh search*": "allow"
|
||||||
|
},
|
||||||
|
"webfetch": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"write": "deny"
|
||||||
|
}
|
||||||
|
run: |
|
||||||
|
TODAY=$(date -u +%Y-%m-%d)
|
||||||
|
|
||||||
|
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
|
||||||
|
|
||||||
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
|
STEP 1: Gather PR data
|
||||||
|
Run these commands to gather PR information:
|
||||||
|
|
||||||
|
# Open PRs with bug fix labels or 'fix' in title
|
||||||
|
gh pr list --repo ${{ github.repository }} --state open --search \"fix in:title\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||||
|
|
||||||
|
# PRs with high activity (get comments separately to filter bots)
|
||||||
|
gh pr list --repo ${{ github.repository }} --state open --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft --limit 100
|
||||||
|
|
||||||
|
# Recently merged bug fixes
|
||||||
|
gh pr list --repo ${{ github.repository }} --state merged --search \"merged:${TODAY} fix in:title\" --json number,title,author,mergedAt --limit 50
|
||||||
|
|
||||||
|
STEP 2: For high-activity PRs, check comment counts
|
||||||
|
For promising PRs, run:
|
||||||
|
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
|
||||||
|
|
||||||
|
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
|
||||||
|
- copilot-pull-request-reviewer
|
||||||
|
- github-actions
|
||||||
|
|
||||||
|
STEP 3: Identify what matters
|
||||||
|
|
||||||
|
**Bug Fixes We Might Miss:**
|
||||||
|
- PRs with 'fix' or 'bug' in title that have been open 2+ days
|
||||||
|
- Small bug fixes (< 100 lines changed) that are easy to review
|
||||||
|
- Bug fixes from community contributors (not core team)
|
||||||
|
|
||||||
|
**High Activity PRs:**
|
||||||
|
- PRs with 5+ human comments (excluding bots listed above)
|
||||||
|
- PRs with back-and-forth discussion
|
||||||
|
- Controversial or complex changes getting attention
|
||||||
|
|
||||||
|
**Quick Wins:**
|
||||||
|
- Small PRs (< 50 lines) that are approved or nearly approved
|
||||||
|
- Bug fixes that just need a final review
|
||||||
|
|
||||||
|
STEP 4: Generate the recap
|
||||||
|
Create a structured recap:
|
||||||
|
|
||||||
|
===DISCORD_START===
|
||||||
|
**Daily PR Recap - ${TODAY}**
|
||||||
|
|
||||||
|
**Bug Fixes Needing Attention**
|
||||||
|
[PRs fixing bugs that might be overlooked - prioritize by age and size]
|
||||||
|
|
||||||
|
**High Activity** (5+ human comments)
|
||||||
|
[PRs with significant discussion - exclude bot comments]
|
||||||
|
|
||||||
|
**Quick Wins** (small, ready to merge)
|
||||||
|
[Easy PRs that just need a review/merge]
|
||||||
|
|
||||||
|
**Merged Bug Fixes Today**
|
||||||
|
[What bug fixes shipped]
|
||||||
|
===DISCORD_END===
|
||||||
|
|
||||||
|
STEP 5: Format for Discord
|
||||||
|
- Use Discord markdown (**, __, etc.)
|
||||||
|
- BE EXTREMELY CONCISE - surface what we might miss
|
||||||
|
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
|
||||||
|
- Include PR author: [#1234](<url>) (@author)
|
||||||
|
- For bug fixes, add brief description of what it fixes
|
||||||
|
- Show line count for quick wins: \"(+15/-3 lines)\"
|
||||||
|
- HARD LIMIT: Keep under 1800 characters total
|
||||||
|
- Skip empty sections
|
||||||
|
- Focus on PRs that need human eyes
|
||||||
|
|
||||||
|
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
|
||||||
|
|
||||||
|
# Extract only the Discord message between markers
|
||||||
|
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
|
||||||
|
|
||||||
|
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Post to Discord
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
||||||
|
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
|
||||||
|
cat /tmp/pr_recap.txt
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read the recap
|
||||||
|
RECAP_RAW=$(cat /tmp/pr_recap.txt)
|
||||||
|
RECAP_LENGTH=${#RECAP_RAW}
|
||||||
|
|
||||||
|
echo "Recap length: ${RECAP_LENGTH} chars"
|
||||||
|
|
||||||
|
# Function to post a message to Discord
|
||||||
|
post_to_discord() {
|
||||||
|
local msg="$1"
|
||||||
|
local content=$(echo "$msg" | jq -Rs '.')
|
||||||
|
curl -s -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d "{\"content\": ${content}}" \
|
||||||
|
"$DISCORD_WEBHOOK_URL"
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If under limit, send as single message
|
||||||
|
if [ "$RECAP_LENGTH" -le 1950 ]; then
|
||||||
|
post_to_discord "$RECAP_RAW"
|
||||||
|
else
|
||||||
|
echo "Splitting into multiple messages..."
|
||||||
|
remaining="$RECAP_RAW"
|
||||||
|
while [ ${#remaining} -gt 0 ]; do
|
||||||
|
if [ ${#remaining} -le 1950 ]; then
|
||||||
|
post_to_discord "$remaining"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
|
||||||
|
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
|
||||||
|
chunk="${remaining:0:$last_newline}"
|
||||||
|
remaining="${remaining:$((last_newline+1))}"
|
||||||
|
else
|
||||||
|
chunk="${remaining:0:1900}"
|
||||||
|
remaining="${remaining:1900}"
|
||||||
|
fi
|
||||||
|
post_to_discord "$chunk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Posted daily PR recap to Discord"
|
||||||
Reference in New Issue
Block a user