name: PR Template Suggester on: pull_request: types: [opened, edited, synchronize] permissions: pull-requests: write contents: read jobs: suggest-template: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Analyze PR files and auto-apply template uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const { data: files } = await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, }); let goFiles = 0, jsFiles = 0, tsFiles = 0, mdFiles = 0, otherFiles = 0; for (const file of files) { const filename = file.filename.toLowerCase(); if (filename.endsWith('.go')) goFiles++; else if (filename.endsWith('.js') || filename.endsWith('.jsx')) jsFiles++; else if (filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.vue')) tsFiles++; else if (filename.endsWith('.md')) mdFiles++; else otherFiles++; } const totalFiles = goFiles + jsFiles + tsFiles + mdFiles + otherFiles; if (totalFiles === 0) { console.log('No files changed'); return; } let suggestedTemplate = null, templateEmoji = '', templateLabel = ''; if (goFiles / totalFiles > 0.5) { suggestedTemplate = 'backend'; templateEmoji = '🔧'; templateLabel = 'backend'; } else if ((jsFiles + tsFiles) / totalFiles > 0.5) { suggestedTemplate = 'frontend'; templateEmoji = '🎨'; templateLabel = 'frontend'; } else if (mdFiles / totalFiles > 0.7) { suggestedTemplate = 'docs'; templateEmoji = '📝'; templateLabel = 'documentation'; } const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, }); const prBody = pr.body || ''; const usesBackendTemplate = prBody.includes('Pull Request - Backend'); const usesFrontendTemplate = prBody.includes('Pull Request - Frontend'); const usesDocsTemplate = prBody.includes('Pull Request - Documentation'); const usesGeneralTemplate = prBody.includes('Pull Request - General'); const usingDefaultTemplate = !usesBackendTemplate && !usesFrontendTemplate && !usesDocsTemplate && !usesGeneralTemplate; if (templateLabel) { try { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels: [templateLabel] }); console.log('Added label: ' + templateLabel); } catch (error) { console.log('Label might not exist, skipping...'); } } function isPRBodyEmpty(body) { if (!body || body.trim().length < 100) return true; const hasEmptyDescription = body.includes('**English:**') && body.match(/\*\*English:\*\*\s*\n\s*\n\s*\n/); const hasEmptyChanges = body.includes('具体变更') && body.match(/\*\*中文:\*\*\s*\n\s*-\s*\n\s*-\s*\n/); if (hasEmptyDescription || hasEmptyChanges) return true; const descMatch = body.match(/\*\*English:\*\*[||]\s*\*\*中文:\*\*\s*\n\s*(.+)/); if (!descMatch || descMatch[1].trim().length < 10) return true; return false; } if (suggestedTemplate && usingDefaultTemplate) { const shouldAutoApply = isPRBodyEmpty(prBody); const templatePath = '.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; if (shouldAutoApply) { try { const { data: templateFile } = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: templatePath, ref: context.payload.pull_request.head.ref }); const templateContent = Buffer.from(templateFile.content, 'base64').toString('utf-8'); await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, body: templateContent }); console.log('Auto-applied ' + suggestedTemplate + ' template'); let fileStats = []; if (goFiles > 0) fileStats.push('- 🔧 Go files: ' + goFiles); if (jsFiles > 0) fileStats.push('- 🎨 JavaScript files: ' + jsFiles); if (tsFiles > 0) fileStats.push('- 🎨 TypeScript files: ' + tsFiles); if (mdFiles > 0) fileStats.push('- 📝 Markdown files: ' + mdFiles); if (otherFiles > 0) fileStats.push('- 📦 Other files: ' + otherFiles); const fileStatsText = fileStats.join('\n'); const notifyComment = '## ' + templateEmoji + ' 已自动应用专用模板 | Auto-Applied Template\n\n' + '检测到您的PR主要包含 **' + suggestedTemplate + '** 相关的变更,系统已自动为您应用相应的模板。\n\n' + 'Detected that your PR primarily contains **' + suggestedTemplate + '** changes. The appropriate template has been automatically applied.\n\n' + '**文件统计 | File Statistics**\n' + fileStatsText + '\n\n' + '**已应用模板 | Applied Template**\n`' + templatePath + '`\n\n' + '✨ 您现在可以直接在PR描述中填写相关信息了!\n\n' + '✨ You can now fill in the relevant information in the PR description!'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: notifyComment }); } catch (error) { console.log('Failed to fetch or apply template: ' + error.message); const templateUrl = 'https://raw.githubusercontent.com/' + context.repo.owner + '/' + context.repo.repo + '/dev/.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; const fallbackComment = '## ' + templateEmoji + ' 建议使用专用模板 | Suggested Template\n\n' + '您的PR主要包含 **' + suggestedTemplate + '** 相关的变更。\n\n' + '**推荐模板 | Recommended Template:** `.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md`\n\n' + '**如何使用 | How to use:** [点击查看模板内容](' + templateUrl + ')'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: fallbackComment }); } } else { console.log('PR body has content, sending suggestion only'); let fileStats = []; if (goFiles > 0) fileStats.push('- 🔧 Go files: ' + goFiles); if (jsFiles > 0) fileStats.push('- 🎨 JavaScript files: ' + jsFiles); if (tsFiles > 0) fileStats.push('- 🎨 TypeScript files: ' + tsFiles); if (mdFiles > 0) fileStats.push('- 📝 Markdown files: ' + mdFiles); if (otherFiles > 0) fileStats.push('- 📦 Other files: ' + otherFiles); const fileStatsText = fileStats.join('\n'); const templateUrl = 'https://raw.githubusercontent.com/' + context.repo.owner + '/' + context.repo.repo + '/dev/.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; const comment = '## ' + templateEmoji + ' 建议使用专用模板 | Suggested Template\n\n' + '您的PR主要包含 **' + suggestedTemplate + '** 相关的变更。我们建议使用更适合的模板以简化填写。\n\n' + 'Your PR primarily contains **' + suggestedTemplate + '** changes. We suggest using a more suitable template to simplify filling.\n\n' + '**文件统计 | File Statistics**\n' + fileStatsText + '\n\n' + '**推荐模板 | Recommended Template**\n```\n.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md\n```\n\n' + '**如何使用 | How to use**\n' + '1. 编辑PR描述 | Edit PR description\n' + '2. 复制 [' + suggestedTemplate + ' 模板内容](' + templateUrl + ') | Copy [' + suggestedTemplate + ' template content](' + templateUrl + ')\n' + '3. 或在创建PR时使用URL参数 | Or use URL parameter when creating PR\n' + ' `?template=' + suggestedTemplate + '.md`\n\n' + '_这是一个自动建议,您可以继续使用当前模板。_\n\n' + '_This is an automated suggestion. You may continue using the current template._'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: comment }); } } else if (suggestedTemplate && !usingDefaultTemplate) { console.log('PR already uses a specific template'); } else { console.log('No specific template suggestion needed - mixed changes'); }