|
| 1 | +name: Issue Welcome Bot |
| 2 | + |
| 3 | +on: |
| 4 | + issues: |
| 5 | + types: [opened] |
| 6 | + |
| 7 | +permissions: |
| 8 | + issues: write |
| 9 | + |
| 10 | +jobs: |
| 11 | + welcome: |
| 12 | + runs-on: ubuntu-latest |
| 13 | + steps: |
| 14 | + - name: Welcome Issue Author |
| 15 | + uses: actions/github-script@v7 |
| 16 | + with: |
| 17 | + script: | |
| 18 | + // Helper function for English ordinal suffix (1st, 2nd, 3rd, 4th...) |
| 19 | + function getOrdinalSuffix(num) { |
| 20 | + const j = num % 10; |
| 21 | + const k = num % 100; |
| 22 | + if (j === 1 && k !== 11) return 'st'; |
| 23 | + if (j === 2 && k !== 12) return 'nd'; |
| 24 | + if (j === 3 && k !== 13) return 'rd'; |
| 25 | + return 'th'; |
| 26 | + } |
| 27 | +
|
| 28 | + const author = context.payload.issue.user.login; |
| 29 | + const issueNumber = context.payload.issue.number; |
| 30 | + const issueTitle = context.payload.issue.title; |
| 31 | + const issueBody = context.payload.issue.body || ''; |
| 32 | +
|
| 33 | + // Check if Chinese characters exist in title |
| 34 | + const hasChinese = /[\u4e00-\u9fff]/.test(issueTitle); |
| 35 | +
|
| 36 | + // Count user's total issues in this repo using search API |
| 37 | + let issueCount = 0; |
| 38 | + let hasValidIssueCount = false; |
| 39 | + let isFirstIssue = false; |
| 40 | +
|
| 41 | + try { |
| 42 | + const { data: searchResult } = await github.rest.search.issuesAndPullRequests({ |
| 43 | + q: `repo:${context.repo.owner}/${context.repo.repo} type:issue author:${author}`, |
| 44 | + }); |
| 45 | + issueCount = searchResult.total_count; |
| 46 | + hasValidIssueCount = true; |
| 47 | + isFirstIssue = issueCount === 1; |
| 48 | + } catch (err) { |
| 49 | + console.log(`Could not get issue count for ${author}, will skip count display`); |
| 50 | + hasValidIssueCount = false; |
| 51 | + } |
| 52 | +
|
| 53 | + // Check if user has starred the repo by listing user's starred |
| 54 | + // repos |
| 55 | + let hasStarred = false; |
| 56 | + try { |
| 57 | + const fullRepoName = `${context.repo.owner}/${context.repo.repo}`; |
| 58 | + let page = 1; |
| 59 | + let found = false; |
| 60 | +
|
| 61 | + while (page <= 10 && !found) { |
| 62 | + const { data: starredRepos } = await github.rest.activity.listReposStarredByUser({ |
| 63 | + username: author, |
| 64 | + per_page: 100, |
| 65 | + page: page |
| 66 | + }); |
| 67 | +
|
| 68 | + if (starredRepos.length === 0) break; |
| 69 | +
|
| 70 | + found = starredRepos.some(repo => repo.full_name === fullRepoName); |
| 71 | + if (found) { |
| 72 | + hasStarred = true; |
| 73 | + console.log(`User ${author} has starred the repo`); |
| 74 | + break; |
| 75 | + } |
| 76 | +
|
| 77 | + page++; |
| 78 | + } |
| 79 | +
|
| 80 | + if (!found) { |
| 81 | + console.log(`User ${author} has not starred the repo`); |
| 82 | + } |
| 83 | + } catch (error) { |
| 84 | + console.log( |
| 85 | + `Could not verify star status for ${author}:`, |
| 86 | + error.message |
| 87 | + ); |
| 88 | + } |
| 89 | +
|
| 90 | + // Check if this is a bug report by title or label |
| 91 | + // Match "bug", "bugs", or "[Bug]:" prefix (case-insensitive) |
| 92 | + const titleContainsBug = /\bbug(s)?\b/i.test(issueTitle) || /^\s*\[bug\]\s*:/i.test(issueTitle); |
| 93 | + const labels = context.payload.issue.labels.map(label => label.name); |
| 94 | + const hasBugLabel = labels.some(name => name && name.toLowerCase() === 'bug'); |
| 95 | + const isBugRelated = titleContainsBug || hasBugLabel; |
| 96 | +
|
| 97 | + // Check if issue uses bug report template |
| 98 | + const usedBugTemplate = issueBody.includes('## CoPaw Version') || |
| 99 | + issueBody.includes('## Environment') || |
| 100 | + issueBody.includes('## Steps to Reproduce'); |
| 101 | +
|
| 102 | + // If bug-related but template not used, need to remind |
| 103 | + const needBugTemplateReminder = isBugRelated && !usedBugTemplate; |
| 104 | +
|
| 105 | + // Build comment |
| 106 | + let comment = ''; |
| 107 | +
|
| 108 | + if (hasChinese) { |
| 109 | + // Bilingual response with logo |
| 110 | + comment = `<div align="center">\n\n`; |
| 111 | + comment += `<img src="https://copaw.agentscope.io/copaw_ip.svg" width="80" height="80" />\n\n`; |
| 112 | + comment += `## 欢迎来到 CoPaw! 🐾 Welcome to CoPaw!\n\n`; |
| 113 | + comment += `</div>\n\n`; |
| 114 | +
|
| 115 | + if (hasValidIssueCount) { |
| 116 | + if (isFirstIssue) { |
| 117 | + comment += `你好 @${author},感谢你提交的第一个 issue!\n`; |
| 118 | + comment += `Hi @${author}, thank you for your first issue!\n\n`; |
| 119 | + } else { |
| 120 | + comment += `你好 @${author},这是你提交的第 ${issueCount} 个 issue。\n`; |
| 121 | + comment += `Hi @${author}, this is your ${issueCount}${getOrdinalSuffix(issueCount)} issue.\n\n`; |
| 122 | + } |
| 123 | + } else { |
| 124 | + comment += `你好 @${author},感谢你提交 issue!\n`; |
| 125 | + comment += `Hi @${author}, thank you for your issue!\n\n`; |
| 126 | + } |
| 127 | +
|
| 128 | + if (needBugTemplateReminder) { |
| 129 | + comment += `### 📋 关于 Bug Report 模板 / About Bug Report Template\n\n`; |
| 130 | + comment += `我们注意到你的 issue 似乎与 bug 相关,但没有使用 Bug Report 模板。为了帮助我们更快地定位和修复问题,请确保你的 bug report 包含以下信息:\n`; |
| 131 | + comment += `We noticed your issue seems to be bug-related, but doesn't use the Bug Report template. To help us reproduce and fix the issue faster, please make sure your bug report includes:\n\n`; |
| 132 | + comment += `- ✅ **CoPaw 版本 / Version** (使用 \`copaw --version\` 查看)\n`; |
| 133 | + comment += `- ✅ **操作系统 / OS** (macOS/Linux/Windows 及版本)\n`; |
| 134 | + comment += `- ✅ **复现步骤 / Steps to Reproduce** (详细的步骤)\n`; |
| 135 | + comment += `- ✅ **实际结果 vs 预期结果 / Actual vs Expected**\n`; |
| 136 | + comment += `- ✅ **日志或截图 / Logs or Screenshots**\n\n`; |
| 137 | + comment += `你可以编辑 issue 来补充这些信息。如果你的 issue 缺少这些信息,维护者可能需要额外时间来询问细节。\n`; |
| 138 | + comment += `You can edit your issue to add this information. Missing information may require maintainers to ask follow-up questions.\n\n`; |
| 139 | + } |
| 140 | +
|
| 141 | + comment += `我们会尽快查看你的 issue。感谢你对 CoPaw 的支持!\n`; |
| 142 | + comment += `We'll review your issue soon. Thank you for supporting CoPaw!\n\n`; |
| 143 | +
|
| 144 | + // Internationalization reminder |
| 145 | + comment += `---\n\n`; |
| 146 | + comment += `> **🌍 关于国际化 / About Internationalization**\n`; |
| 147 | + comment += `> \n`; |
| 148 | + comment += `> CoPaw 是一个国际化的开源社区。我们建议使用英文提交 issue,这样可以让更多的开发者参与讨论和贡献。\n`; |
| 149 | + comment += `> \n`; |
| 150 | + comment += `> CoPaw is an international open-source community. We recommend using English for issues so that more developers worldwide can participate in discussions and contributions.\n\n`; |
| 151 | +
|
| 152 | + // Star reminder at the end |
| 153 | + if (!hasStarred) { |
| 154 | + comment += `> [!TIP]\n`; |
| 155 | + comment += `> **⭐ 如果你觉得 CoPaw 有帮助,请给我们一个 Star!**\n`; |
| 156 | + comment += `> \n`; |
| 157 | + comment += `> **⭐ If you find CoPaw useful, please give us a Star!**\n`; |
| 158 | + comment += `> \n`; |
| 159 | + comment += `> 你的 star 将帮助更多开发者发现这个项目!🐾\n`; |
| 160 | + comment += `> \n`; |
| 161 | + comment += `> Your star helps more developers discover this project! 🐾`; |
| 162 | + } |
| 163 | +
|
| 164 | + } else { |
| 165 | + // English only with logo |
| 166 | + comment = `<div align="center">\n\n`; |
| 167 | + comment += `<img src="https://copaw.agentscope.io/copaw_ip.svg" width="80" height="80" />\n\n`; |
| 168 | + comment += `## Welcome to CoPaw! 🐾\n\n`; |
| 169 | + comment += `</div>\n\n`; |
| 170 | +
|
| 171 | + if (hasValidIssueCount) { |
| 172 | + if (isFirstIssue) { |
| 173 | + comment += `Hi @${author}, thank you for your first issue!\n\n`; |
| 174 | + } else { |
| 175 | + comment += `Hi @${author}, this is your ${issueCount}${getOrdinalSuffix(issueCount)} issue.\n\n`; |
| 176 | + } |
| 177 | + } else { |
| 178 | + comment += `Hi @${author}, thank you for your issue!\n\n`; |
| 179 | + } |
| 180 | +
|
| 181 | + if (needBugTemplateReminder) { |
| 182 | + comment += `### 📋 About Bug Report Template\n\n`; |
| 183 | + comment += `We noticed your issue seems to be bug-related, but doesn't use the Bug Report template. To help us reproduce and fix the issue faster, please make sure your bug report includes:\n\n`; |
| 184 | + comment += `- ✅ **CoPaw Version** (use \`copaw --version\`)\n`; |
| 185 | + comment += `- ✅ **Operating System** (macOS/Linux/Windows and version)\n`; |
| 186 | + comment += `- ✅ **Steps to Reproduce** (detailed steps)\n`; |
| 187 | + comment += `- ✅ **Actual vs Expected Behavior**\n`; |
| 188 | + comment += `- ✅ **Logs or Screenshots**\n\n`; |
| 189 | + comment += `You can edit your issue to add this information. Missing information may require us to ask follow-up questions, which can delay the fix.\n\n`; |
| 190 | + } |
| 191 | +
|
| 192 | + comment += `We'll review your issue soon. Thank you for supporting CoPaw!\n\n`; |
| 193 | +
|
| 194 | + // Star reminder at the end |
| 195 | + if (!hasStarred) { |
| 196 | + comment += `---\n\n`; |
| 197 | + comment += `> [!TIP]\n`; |
| 198 | + comment += `> **⭐ If you find CoPaw useful, please give us a Star!**\n`; |
| 199 | + comment += `> \n`; |
| 200 | + comment += `> Your star helps more developers discover this project! 🐾`; |
| 201 | + } |
| 202 | + } |
| 203 | +
|
| 204 | + // Post comment |
| 205 | + await github.rest.issues.createComment({ |
| 206 | + owner: context.repo.owner, |
| 207 | + repo: context.repo.repo, |
| 208 | + issue_number: issueNumber, |
| 209 | + body: comment |
| 210 | + }); |
| 211 | +
|
| 212 | + const logMessage = hasValidIssueCount |
| 213 | + ? `Posted welcome comment on issue #${issueNumber} for @${author} (issue #${issueCount})` |
| 214 | + : `Posted welcome comment on issue #${issueNumber} for @${author}`; |
| 215 | + console.log(logMessage); |
0 commit comments