daily-pr-recap.yml 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. name: daily-pr-recap
  2. on:
  3. schedule:
  4. # Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
  5. - cron: "0 22 * * *"
  6. workflow_dispatch: # Allow manual trigger for testing
  7. jobs:
  8. pr-recap:
  9. runs-on: blacksmith-4vcpu-ubuntu-2404
  10. permissions:
  11. contents: read
  12. pull-requests: read
  13. steps:
  14. - name: Checkout repository
  15. uses: actions/checkout@v4
  16. with:
  17. fetch-depth: 1
  18. - uses: ./.github/actions/setup-bun
  19. - name: Install opencode
  20. run: curl -fsSL https://opencode.ai/install | bash
  21. - name: Generate daily PR recap
  22. id: recap
  23. env:
  24. OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
  25. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  26. OPENCODE_PERMISSION: |
  27. {
  28. "bash": {
  29. "*": "deny",
  30. "gh pr*": "allow",
  31. "gh search*": "allow"
  32. },
  33. "webfetch": "deny",
  34. "edit": "deny",
  35. "write": "deny"
  36. }
  37. run: |
  38. TODAY=$(date -u +%Y-%m-%d)
  39. opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
  40. TODAY'S DATE: ${TODAY}
  41. STEP 1: Gather PR data
  42. Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
  43. # PRs created today
  44. gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
  45. # PRs with activity today (updated today)
  46. gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
  47. STEP 2: For high-activity PRs, check comment counts
  48. For promising PRs, run:
  49. gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
  50. IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
  51. - copilot-pull-request-reviewer
  52. - github-actions
  53. STEP 3: Identify what matters (ONLY from today's PRs)
  54. **Bug Fixes From Today:**
  55. - PRs with 'fix' or 'bug' in title created/updated today
  56. - Small bug fixes (< 100 lines changed) that are easy to review
  57. - Bug fixes from community contributors
  58. **High Activity Today:**
  59. - PRs with significant human comments today (excluding bots listed above)
  60. - PRs with back-and-forth discussion today
  61. **Quick Wins:**
  62. - Small PRs (< 50 lines) that are approved or nearly approved
  63. - PRs that just need a final review
  64. STEP 4: Generate the recap
  65. Create a structured recap:
  66. ===DISCORD_START===
  67. **Daily PR Recap - ${TODAY}**
  68. **New PRs Today**
  69. [PRs opened today - group by type: bug fixes, features, etc.]
  70. **Active PRs Today**
  71. [PRs with activity/updates today - significant discussion]
  72. **Quick Wins**
  73. [Small PRs ready to merge]
  74. ===DISCORD_END===
  75. STEP 5: Format for Discord
  76. - Use Discord markdown (**, __, etc.)
  77. - BE EXTREMELY CONCISE - surface what we might miss
  78. - Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
  79. - Include PR author: [#1234](<url>) (@author)
  80. - For bug fixes, add brief description of what it fixes
  81. - Show line count for quick wins: \"(+15/-3 lines)\"
  82. - HARD LIMIT: Keep under 1800 characters total
  83. - Skip empty sections
  84. - Focus on PRs that need human eyes
  85. 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
  86. # Extract only the Discord message between markers
  87. sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
  88. echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
  89. - name: Post to Discord
  90. env:
  91. DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
  92. run: |
  93. if [ -z "$DISCORD_WEBHOOK_URL" ]; then
  94. echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
  95. cat /tmp/pr_recap.txt
  96. exit 0
  97. fi
  98. # Read the recap
  99. RECAP_RAW=$(cat /tmp/pr_recap.txt)
  100. RECAP_LENGTH=${#RECAP_RAW}
  101. echo "Recap length: ${RECAP_LENGTH} chars"
  102. # Function to post a message to Discord
  103. post_to_discord() {
  104. local msg="$1"
  105. local content=$(echo "$msg" | jq -Rs '.')
  106. curl -s -H "Content-Type: application/json" \
  107. -X POST \
  108. -d "{\"content\": ${content}}" \
  109. "$DISCORD_WEBHOOK_URL"
  110. sleep 1
  111. }
  112. # If under limit, send as single message
  113. if [ "$RECAP_LENGTH" -le 1950 ]; then
  114. post_to_discord "$RECAP_RAW"
  115. else
  116. echo "Splitting into multiple messages..."
  117. remaining="$RECAP_RAW"
  118. while [ ${#remaining} -gt 0 ]; do
  119. if [ ${#remaining} -le 1950 ]; then
  120. post_to_discord "$remaining"
  121. break
  122. else
  123. chunk="${remaining:0:1900}"
  124. last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
  125. if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
  126. chunk="${remaining:0:$last_newline}"
  127. remaining="${remaining:$((last_newline+1))}"
  128. else
  129. chunk="${remaining:0:1900}"
  130. remaining="${remaining:1900}"
  131. fi
  132. post_to_discord "$chunk"
  133. fi
  134. done
  135. fi
  136. echo "Posted daily PR recap to Discord"