test(providers): add regression guard for Aliyun Coding Plan catalog alignment (#6551) #2694
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Welcome Bot | |
| on: | |
| pull_request_target: | |
| types: [opened] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| welcome: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Welcome and Label First-Time PR Author | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const author = context.payload.pull_request.user.login; | |
| const prNumber = context.payload.pull_request.number; | |
| const prTitle = context.payload.pull_request.title; | |
| const prBody = context.payload.pull_request.body || ''; | |
| // --------------------------------------------------------------- | |
| // Spam gate: close if author has >10 open PRs in this repo. | |
| // Bypassed for repo maintainers and admins. | |
| // per_page=1 is sufficient — we only need total_count. | |
| // --------------------------------------------------------------- | |
| try { | |
| // Bypass for repo maintainers / admins. | |
| try { | |
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: author, | |
| }); | |
| if (['admin', 'maintain'].includes(perm.permission)) { | |
| console.log(`@${author} is a repo ${perm.permission} — bypassing spam gate`); | |
| throw new Error('__bypass__'); | |
| } | |
| } catch (permErr) { | |
| if (permErr.message === '__bypass__') throw permErr; | |
| // 404 = not a collaborator, proceed with spam check. | |
| if (permErr.status !== 404) { | |
| console.log(`Permission check warning: ${permErr.message} — proceeding with spam check`); | |
| } | |
| } | |
| const { data: openResult } = await github.rest.search.issuesAndPullRequests({ | |
| q: `repo:${context.repo.owner}/${context.repo.repo} type:pr is:open author:${author}`, | |
| per_page: 1, | |
| }); | |
| const openCount = openResult.total_count; | |
| console.log(`@${author} has ${openCount} open PR(s) in this repo`); | |
| if (openCount > 10) { | |
| // Ensure the label exists (create it if missing) | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'Close-and-review-later', | |
| color: 'e4e669', | |
| description: 'Closed automatically for later review', | |
| }); | |
| } catch (labelErr) { | |
| // 422 = label already exists, that's fine | |
| if (labelErr.status !== 422) console.log(`Label creation warning: ${labelErr.message}`); | |
| } | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['Close-and-review-later'], | |
| }); | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| state: 'closed', | |
| }); | |
| const warningComment = [ | |
| `<div align="center">`, | |
| ``, | |
| `<img src="https://img.alicdn.com/imgextra/i2/O1CN01eSmIfy1hJImze8NN8_!!6000000004256-2-tps-1858-1858.png" width="80" height="80" />`, | |
| ``, | |
| `</div>`, | |
| ``, | |
| `Hi @${author},`, | |
| ``, | |
| `This pull request has been **automatically closed** because you currently have more than **10 open pull requests** in this repository.`, | |
| ``, | |
| `> **Why this happened**`, | |
| `> Our [contribution policy](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/4333) asks contributors to maintain a manageable number of active pull requests to ensure fair use of maintainer resources. A high volume of simultaneous open PRs — especially those that are AI-generated and submitted without personal verification — places a significant burden on maintainers.`, | |
| ``, | |
| `**What you can do:**`, | |
| `1. Review your open pull requests and close any that are no longer relevant, are duplicated, or have not been personally verified.`, | |
| `2. Once you have fewer than 10 open pull requests, you are welcome to reopen this PR or submit a new one.`, | |
| `3. Please ensure every PR you submit has been **personally tested**, addresses a verified issue, and follows our [pull request guidelines](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/4333).`, | |
| ``, | |
| `This pull request has been labeled \`Close-and-review-later\` and may be revisited by a maintainer.`, | |
| ``, | |
| `Thank you for your understanding. 🐾`, | |
| ``, | |
| `---`, | |
| ``, | |
| `你好 @${author},`, | |
| ``, | |
| `由于你目前在本仓库有超过 **10 个处于 open 状态的 PR**,此 pull request 已被**自动关闭**。`, | |
| ``, | |
| `> **原因说明**`, | |
| `> 根据我们的[贡献规范](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/4333),为保障维护者资源的合理使用,我们要求贡献者控制活跃 PR 的数量。大量未经人工验证的 open PR(尤其是 AI 生成内容)会给维护者带来沉重负担。`, | |
| ``, | |
| `**你可以采取以下措施:**`, | |
| `1. 检查并关闭不再相关、重复或未经本人验证的 open PR。`, | |
| `2. 当你的 open PR 数量少于 10 个时,欢迎重新开启此 PR 或提交新的 PR。`, | |
| `3. 请确保每个 PR 均经过**本人亲自测试**,针对已验证的问题,并遵循我们的 [PR 提交规范](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/4333)。`, | |
| ``, | |
| `此 PR 已被标记为 \`Close-and-review-later\`,维护者可能会在后续进行审核。`, | |
| ``, | |
| `感谢你的理解与配合。🐾`, | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: warningComment, | |
| }); | |
| console.log(`Closed PR #${prNumber} for @${author} — open PR limit exceeded (${openCount} open)`); | |
| return; | |
| } | |
| } catch (spamErr) { | |
| if (spamErr.message !== '__bypass__') { | |
| // Fail open: if the check itself errors, don't block legitimate contributors. | |
| console.log(`Spam gate check failed: ${spamErr.message} — continuing with normal welcome`); | |
| } | |
| } | |
| // --------------------------------------------------------------- | |
| // Check if Chinese characters exist in title | |
| const hasChinese = /[\u4e00-\u9fff]/.test(prTitle); | |
| // Helper function for English ordinal suffix (1st, 2nd, 3rd, 4th...) | |
| function getOrdinalSuffix(num) { | |
| const j = num % 10; | |
| const k = num % 100; | |
| if (j === 1 && k !== 11) return 'st'; | |
| if (j === 2 && k !== 12) return 'nd'; | |
| if (j === 3 && k !== 13) return 'rd'; | |
| return 'th'; | |
| } | |
| // Count user's total PRs in this repo using search API | |
| let prCount = 0; | |
| let hasValidPRCount = false; | |
| try { | |
| const { data: searchResult } = await github.rest.search.issuesAndPullRequests({ | |
| q: `repo:${context.repo.owner}/${context.repo.repo} type:pr author:${author}`, | |
| }); | |
| prCount = searchResult.total_count; | |
| hasValidPRCount = true; | |
| } catch (err) { | |
| console.log(`Could not get PR count for ${author}, will skip count display`); | |
| hasValidPRCount = false; | |
| } | |
| // Check if this is user's first PR (for labeling) | |
| let isFirstPR = false; | |
| try { | |
| const { data: searchResult } = await github.rest.search.issuesAndPullRequests({ | |
| q: `repo:${context.repo.owner}/${context.repo.repo} type:pr author:${author} is:closed`, | |
| }); | |
| isFirstPR = searchResult.total_count === 0; | |
| // Add first-time-contributor label only for first PR | |
| if (isFirstPR) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['first-time-contributor'], | |
| }); | |
| console.log(`Labeled PR #${prNumber} as first-time-contributor`); | |
| } | |
| } catch (err) { | |
| if (err.status === 422 && err.message && err.message.includes('cannot be searched')) { | |
| console.log(`Search API cannot look up author ${author}, continuing without label`); | |
| } else { | |
| throw err; | |
| } | |
| } | |
| // Check if user has starred the repo by listing user's starred | |
| // repos | |
| let hasStarred = false; | |
| try { | |
| const fullRepoName = `${context.repo.owner}/${context.repo.repo}`; | |
| let page = 1; | |
| let found = false; | |
| while (page <= 10 && !found) { | |
| const { data: starredRepos } = await github.rest.activity.listReposStarredByUser({ | |
| username: author, | |
| per_page: 100, | |
| page: page | |
| }); | |
| if (starredRepos.length === 0) break; | |
| found = starredRepos.some(repo => repo.full_name === fullRepoName); | |
| if (found) { | |
| hasStarred = true; | |
| console.log(`User ${author} has starred the repo`); | |
| break; | |
| } | |
| page++; | |
| } | |
| if (!found) { | |
| console.log(`User ${author} has not starred the repo`); | |
| } | |
| } catch (error) { | |
| console.log( | |
| `Could not verify star status for ${author}:`, | |
| error.message | |
| ); | |
| } | |
| // Check if PR uses template properly | |
| const hasDescription = prBody.includes('## Description') && | |
| prBody.length > 100; | |
| const hasTypeOfChange = prBody.includes('## Type of Change'); | |
| const hasComponents = prBody.includes('## Component(s) Affected'); | |
| const hasChecklist = prBody.includes('## Checklist'); | |
| const hasTesting = prBody.includes('## Testing'); | |
| const usedTemplate = hasDescription && hasTypeOfChange && | |
| hasComponents && hasChecklist; | |
| // Calculate milestone celebration (shared by both branches) | |
| // 🏆 = 100, 🎖️ = 50, 🏅 = 10 | |
| let isMilestone = false; | |
| let medalEmoji = ''; | |
| if (hasValidPRCount && !isFirstPR && prCount % 10 === 0) { | |
| isMilestone = true; | |
| // Calculate medal composition | |
| const hundreds = Math.floor(prCount / 100); | |
| const remainder1 = prCount % 100; | |
| const fifties = Math.floor(remainder1 / 50); | |
| const remainder2 = remainder1 % 50; | |
| const tens = Math.floor(remainder2 / 10); | |
| medalEmoji = '🏆'.repeat(hundreds) + '🎖️'.repeat(fifties) + '🏅'.repeat(tens); | |
| } | |
| // Build comment | |
| let comment = ''; | |
| if (hasChinese) { | |
| // Bilingual response with logo | |
| comment = `<div align="center">\n\n`; | |
| comment += `<img src="https://img.alicdn.com/imgextra/i2/O1CN01eSmIfy1hJImze8NN8_!!6000000004256-2-tps-1858-1858.png" width="80" height="80" />\n\n`; | |
| comment += `## 欢迎来到 QwenPaw! 🐾 Welcome to QwenPaw!\n\n`; | |
| comment += `</div>\n\n`; | |
| if (hasValidPRCount) { | |
| if (isFirstPR) { | |
| comment += `你好 @${author},感谢你提交的第一个 Pull Request!🎉\n`; | |
| comment += `Hi @${author}, thank you for your first Pull Request! 🎉\n\n`; | |
| } else { | |
| comment += `你好 @${author},这是你提交的第 ${prCount} 个 Pull Request。\n`; | |
| comment += `Hi @${author}, this is your ${prCount}${getOrdinalSuffix(prCount)} Pull Request.\n\n`; | |
| // Add milestone celebration if applicable | |
| if (isMilestone) { | |
| comment += `### 🎁 里程碑庆祝 / Milestone Celebration\n\n`; | |
| comment += `${medalEmoji} 恭喜!这是你的第 ${prCount} 个贡献,感谢你对 QwenPaw 的持续支持!你是 QwenPaw 社区的重要成员!🐾\n`; | |
| comment += `${medalEmoji} Congratulations! This is your ${prCount}${getOrdinalSuffix(prCount)} contribution. Thank you for your continued support! You're an important member of the QwenPaw community! 🐾\n\n`; | |
| } | |
| } | |
| } else { | |
| comment += `你好 @${author},感谢你的 Pull Request!🎉\n`; | |
| comment += `Hi @${author}, thank you for your Pull Request! 🎉\n\n`; | |
| } | |
| if (!usedTemplate) { | |
| comment += `### 📋 关于 Pull Request 模板 / About PR Template\n\n`; | |
| comment += `为了帮助维护者更快地审查你的 PR,请确保包含以下信息:\n`; | |
| comment += `To help maintainers review your PR faster, please make sure to include:\n\n`; | |
| comment += `- ✅ **描述 / Description** - 说明这个 PR 做了什么以及为什么 / What this PR does and why\n`; | |
| comment += `- ✅ **变更类型 / Type of Change** - Bug fix / Feature / Breaking change / Docs / Refactoring\n`; | |
| comment += `- ✅ **影响的组件 / Component(s) Affected** - Core / Console / Channels / Skills / CLI / Docs / Tests / CI/CD\n`; | |
| comment += `- ✅ **检查清单 / Checklist**:\n`; | |
| comment += ` - 运行 \`pre-commit run --all-files\` 并通过 / Run and pass \`pre-commit run --all-files\`\n`; | |
| comment += ` - 运行相关测试并通过 / Run and pass relevant tests\n`; | |
| comment += ` - 更新文档(如需要)/ Update docs if needed\n`; | |
| comment += `- ✅ **测试方法 / Testing** - 如何测试这些变更 / How to test these changes\n`; | |
| comment += `- ✅ **本地验证证据 / Local Verification Evidence**:\n`; | |
| comment += ` \`\`\`bash\n`; | |
| comment += ` pre-commit run --all-files\n`; | |
| comment += ` # paste summary result\n`; | |
| comment += ` \n`; | |
| comment += ` pytest\n`; | |
| comment += ` # paste summary result\n`; | |
| comment += ` \`\`\`\n\n`; | |
| comment += `完整的 PR 信息可以加快审查速度。你可以编辑 PR 描述来补充这些内容。\n`; | |
| comment += `Complete PR information helps speed up the review process. You can edit the PR description to add these details.\n\n`; | |
| } else if (usedTemplate && !hasTesting) { | |
| comment += `### 💡 小提示 / Quick Tip\n\n`; | |
| comment += `看起来你使用了 PR 模板,太棒了!别忘了填写 **Testing** 部分,说明如何测试你的变更。\n`; | |
| comment += `Great job using the PR template! Don't forget to fill in the **Testing** section with how to test your changes.\n\n`; | |
| } | |
| comment += `### 🙌 加入开发者社区 / Join Developer Community\n\n`; | |
| comment += `非常感谢你的贡献!我们诚挚地邀请你加入 QwenPaw 官方开发者群!\n`; | |
| comment += `Thanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group!\n\n`; | |
| comment += `你可以在我们的文档页面的 "Developer Community" 部分找到 Discord 和钉钉群链接:\n`; | |
| comment += `You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page:\n`; | |
| comment += `https://qwenpaw.agentscope.io/docs/community\n\n`; | |
| comment += `我们真诚地感谢你的热情,也期待你未来的更多贡献!😊\n`; | |
| comment += `We truly appreciate your enthusiasm—and look forward to your future contributions! 😊\n\n`; | |
| comment += `我们会尽快审查你的 PR。\n`; | |
| comment += `We'll review your PR soon.\n\n`; | |
| // Star reminder at the end | |
| if (!hasStarred) { | |
| comment += `---\n\n`; | |
| comment += `> [!TIP]\n`; | |
| comment += `> **⭐ 如果你觉得 QwenPaw 有帮助,请给我们一个 Star!**\n`; | |
| comment += `> \n`; | |
| comment += `> **⭐ If you find QwenPaw useful, please give us a Star!**\n`; | |
| comment += `> \n`; | |
| comment += `> <div align="center">\n`; | |
| comment += `> \n`; | |
| comment += `> <a href="https://github.com/agentscope-ai/QwenPaw"><img src="https://img.alicdn.com/imgextra/i1/O1CN01V8HYv61By0HYcIDaq_!!6000000000013-1-tps-1698-954.gif" alt="Star QwenPaw" width="600" /></a>\n`; | |
| comment += `> \n`; | |
| comment += `> </div>\n`; | |
| comment += `> \n`; | |
| comment += `> **掌握最新动态 / Staying ahead**\n`; | |
| comment += `> \n`; | |
| comment += `> 在 GitHub 上 Star QwenPaw,第一时间收到新版本发布通知。\n`; | |
| comment += `> \n`; | |
| comment += `> Star QwenPaw on GitHub and be instantly notified of new releases.\n`; | |
| comment += `> \n`; | |
| comment += `> 你的 star 将帮助更多开发者发现这个项目!🐾\n`; | |
| comment += `> \n`; | |
| comment += `> Your star helps more developers discover this project! 🐾`; | |
| } | |
| } else { | |
| // English only with logo | |
| comment = `<div align="center">\n\n`; | |
| comment += `<img src="https://img.alicdn.com/imgextra/i2/O1CN01eSmIfy1hJImze8NN8_!!6000000004256-2-tps-1858-1858.png" width="80" height="80" />\n\n`; | |
| comment += `## Welcome to QwenPaw! 🐾\n\n`; | |
| comment += `</div>\n\n`; | |
| if (hasValidPRCount) { | |
| if (isFirstPR) { | |
| comment += `Hi @${author}, thank you for your first Pull Request! 🎉\n\n`; | |
| } else { | |
| comment += `Hi @${author}, this is your ${prCount}${getOrdinalSuffix(prCount)} Pull Request.\n\n`; | |
| // Add milestone celebration if applicable | |
| if (isMilestone) { | |
| comment += `### 🎁 Milestone Celebration\n\n`; | |
| comment += `${medalEmoji} Congratulations! This is your ${prCount}${getOrdinalSuffix(prCount)} contribution. Thank you for your continued support! You're an important member of the QwenPaw community! 🐾\n\n`; | |
| } | |
| } | |
| } else { | |
| comment += `Hi @${author}, thank you for your Pull Request! 🎉\n\n`; | |
| } | |
| if (!usedTemplate) { | |
| comment += `### 📋 About PR Template\n\n`; | |
| comment += `To help maintainers review your PR faster, please make sure to include:\n\n`; | |
| comment += `- ✅ **Description** - What this PR does and why\n`; | |
| comment += `- ✅ **Type of Change** - Bug fix / Feature / Breaking change / Documentation / Refactoring\n`; | |
| comment += `- ✅ **Component(s) Affected** - Core / Console / Channels / Skills / CLI / Documentation / Tests / CI/CD / Scripts\n`; | |
| comment += `- ✅ **Checklist**:\n`; | |
| comment += ` - Run and pass \`pre-commit run --all-files\`\n`; | |
| comment += ` - Run and pass relevant tests (\`pytest\` or as applicable)\n`; | |
| comment += ` - Update documentation if needed\n`; | |
| comment += `- ✅ **Testing** - How to test these changes\n`; | |
| comment += `- ✅ **Local Verification Evidence**:\n`; | |
| comment += ` \`\`\`bash\n`; | |
| comment += ` pre-commit run --all-files\n`; | |
| comment += ` # paste summary result\n`; | |
| comment += ` \n`; | |
| comment += ` pytest\n`; | |
| comment += ` # paste summary result\n`; | |
| comment += ` \`\`\`\n\n`; | |
| comment += `Complete PR information helps speed up the review process. You can edit the PR description to add these details.\n\n`; | |
| } else if (usedTemplate && !hasTesting) { | |
| comment += `### 💡 Quick Tip\n\n`; | |
| comment += `Great job using the PR template! Don't forget to fill in the **Testing** section with how to test your changes.\n\n`; | |
| } | |
| comment += `### 🙌 Join Developer Community\n\n`; | |
| comment += `Thanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group! You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page:\n`; | |
| comment += `https://qwenpaw.agentscope.io/docs/community\n\n`; | |
| comment += `We truly appreciate your enthusiasm—and look forward to your future contributions! 😊\n\n`; | |
| comment += `We'll review your PR soon.\n\n`; | |
| // Star reminder at the end | |
| if (!hasStarred) { | |
| comment += `---\n\n`; | |
| comment += `> [!TIP]\n`; | |
| comment += `> **⭐ If you find QwenPaw useful, please give us a Star!**\n`; | |
| comment += `> \n`; | |
| comment += `> <div align="center">\n`; | |
| comment += `> \n`; | |
| comment += `> <a href="https://github.com/agentscope-ai/QwenPaw"><img src="https://img.alicdn.com/imgextra/i1/O1CN01V8HYv61By0HYcIDaq_!!6000000000013-1-tps-1698-954.gif" alt="Star QwenPaw" width="600" /></a>\n`; | |
| comment += `> \n`; | |
| comment += `> </div>\n`; | |
| comment += `> \n`; | |
| comment += `> **Staying ahead**\n`; | |
| comment += `> \n`; | |
| comment += `> Star QwenPaw on GitHub and be instantly notified of new releases.\n`; | |
| comment += `> \n`; | |
| comment += `> Your star helps more developers discover this project! 🐾`; | |
| } | |
| } | |
| // Post comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: comment | |
| }); | |
| const logMessage = hasValidPRCount | |
| ? `Posted welcome comment on PR #${prNumber} for @${author} (PR #${prCount}${isFirstPR ? ', first-time contributor' : ''})` | |
| : `Posted welcome comment on PR #${prNumber} for @${author}${isFirstPR ? ' (first-time contributor)' : ''}`; | |
| console.log(logMessage); |