|
| 1 | +name: Auto-respond to Issues |
| 2 | + |
| 3 | +on: |
| 4 | + issues: |
| 5 | + types: [opened] |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + issue_number: |
| 9 | + description: 'Number of issue to handle' |
| 10 | + required: true |
| 11 | + type: string |
| 12 | + |
| 13 | +permissions: |
| 14 | + issues: write |
| 15 | + contents: read |
| 16 | + |
| 17 | +jobs: |
| 18 | + auto-respond: |
| 19 | + runs-on: ubuntu-latest |
| 20 | + steps: |
| 21 | + - name: Checkout repository |
| 22 | + uses: actions/checkout@v4 |
| 23 | + |
| 24 | + - name: Setup Node.js |
| 25 | + uses: actions/setup-node@v4 |
| 26 | + with: |
| 27 | + node-version: '18' |
| 28 | + |
| 29 | + - name: Install dependencies |
| 30 | + run: | |
| 31 | + npm install axios @octokit/rest |
| 32 | +
|
| 33 | + - name: Process issue and generate response |
| 34 | + id: process-issue |
| 35 | + uses: actions/github-script@v7 |
| 36 | + with: |
| 37 | + script: | |
| 38 | + const axios = require('axios'); |
| 39 | + |
| 40 | + // Get issue details - handle both automatic and manual triggers |
| 41 | + let issue, issueTitle, issueBody, issueNumber, issueAuthor; |
| 42 | + |
| 43 | + if (context.payload.inputs?.issue_number) { |
| 44 | + // Manual trigger - fetch issue details |
| 45 | + const issueResponse = await github.rest.issues.get({ |
| 46 | + owner: context.repo.owner, |
| 47 | + repo: context.repo.repo, |
| 48 | + issue_number: parseInt(context.payload.inputs.issue_number) |
| 49 | + }); |
| 50 | + issue = issueResponse.data; |
| 51 | + issueTitle = issue.title; |
| 52 | + issueBody = issue.body || ''; |
| 53 | + issueNumber = issue.number; |
| 54 | + issueAuthor = issue.user.login; |
| 55 | + } else { |
| 56 | + // Automatic trigger - use context |
| 57 | + issue = context.payload.issue; |
| 58 | + issueTitle = issue.title; |
| 59 | + issueBody = issue.body || ''; |
| 60 | + issueNumber = issue.number; |
| 61 | + issueAuthor = issue.user.login; |
| 62 | + } |
| 63 | + |
| 64 | + console.log(`Processing issue #${issueNumber}: ${issueTitle}`); |
| 65 | + |
| 66 | + // Skip if issue is from a maintainer/collaborator |
| 67 | + const collaborators = ['pwizla']; |
| 68 | + const isTesting = issueTitle.includes('[TEST]') || issueBody.includes('testing-auto-response'); |
| 69 | + |
| 70 | + if (collaborators.includes(issueAuthor) && !isTesting) { |
| 71 | + console.log('Issue from maintainer, skipping auto-response'); |
| 72 | + return; |
| 73 | + } |
| 74 | + |
| 75 | + // Prepare the enhanced query for Kapa AI |
| 76 | + const cleanedBody = issueBody |
| 77 | + .replace(/<!--[\s\S]*?-->/g, '') // Remove HTML comments |
| 78 | + .replace(/\n_No response_\s*$/gm, '') // Remove "No response" placeholders |
| 79 | + .replace(/Automatic sync of commit from main/g, '') // Remove auto-sync indicators |
| 80 | + .replace(/\n{3,}/g, '\n\n') // Clean up multiple newlines |
| 81 | + .trim(); |
| 82 | + |
| 83 | + const finalBody = cleanedBody.length < 20 ? |
| 84 | + `${cleanedBody}\n\n(Note: This is a brief issue description. The title "${issueTitle}" may contain additional context.)` : |
| 85 | + cleanedBody; |
| 86 | + |
| 87 | + const query = [ |
| 88 | + "I need to provide a helpful, friendly, and comprehensive response to a GitHub issue from the Strapi community.", |
| 89 | + "", |
| 90 | + "**Issue Title:** " + issueTitle, |
| 91 | + "", |
| 92 | + "**Issue Description:**", |
| 93 | + finalBody, |
| 94 | + "", |
| 95 | + "Please provide a response that:", |
| 96 | + "", |
| 97 | + "1. **Tone & Approach:**", |
| 98 | + " - Be warm, welcoming, and supportive - this represents the Strapi brand", |
| 99 | + " - Show empathy for the user's situation and acknowledge their effort in reporting the issue", |
| 100 | + " - Use friendly language that makes the user feel valued in our community", |
| 101 | + " - Keep the introduction concise and professional - avoid overly lengthy greetings", |
| 102 | + "2. **Technical Content:**", |
| 103 | + " - Directly address the question or problem described", |
| 104 | + " - Provide relevant code examples, configuration snippets, or step-by-step guidance when applicable", |
| 105 | + " - Include links to official Strapi documentation that can help", |
| 106 | + " - If the issue involves a bug, acknowledge it and provide workarounds if possible", |
| 107 | + " - If it's a feature request, explain current alternatives or suggest next steps", |
| 108 | + "", |
| 109 | + "3. **Response Guidelines:**", |
| 110 | + " - If you cannot find specific information to answer this question, be honest about it", |
| 111 | + " - Suggest where the user might find more resources (Discord, documentation sections, etc.)", |
| 112 | + " - For complex issues, break down the response into clear, actionable steps", |
| 113 | + " - If the issue seems like it requires core team attention, indicate that appropriately", |
| 114 | + " - If suggesting feature requests or improvements, direct users to https://feedback.strapi.io", |
| 115 | + " - If the issue seems to be a product bug or core functionality issue rather than documentation, clearly mention this and format the username as `@pwizla` (with backticks) for potential transfer to strapi/strapi repository", "", |
| 116 | + "Please craft a response that will be posted as an automated GitHub comment, so it should be complete and helpful on its own while being genuinely friendly and supportive." |
| 117 | + ].join('\n'); |
| 118 | + |
| 119 | + try { |
| 120 | + // Call Kapa AI Chat API |
| 121 | + const kapaApiUrl = `https://api.kapa.ai/query/v1/projects/${process.env.KAPA_PROJECT_ID}/chat/`; |
| 122 | + |
| 123 | + console.log(`Calling Kapa Chat API: ${kapaApiUrl}`); |
| 124 | + |
| 125 | + const kapaResponse = await axios.post(kapaApiUrl, { |
| 126 | + query: query, |
| 127 | + user_data: { |
| 128 | + source: 'github-automation', |
| 129 | + issue_number: issueNumber, |
| 130 | + author: issueAuthor |
| 131 | + } |
| 132 | + }, { |
| 133 | + headers: { |
| 134 | + 'X-API-Key': process.env.KAPA_API_TOKEN, |
| 135 | + 'Content-Type': 'application/json', |
| 136 | + 'User-Agent': 'Strapi-Docs-GitHub-Bot/1.0' |
| 137 | + }, |
| 138 | + timeout: 120000 // 2-minute timeout |
| 139 | + }); |
| 140 | + |
| 141 | + console.log('Kapa API response received'); |
| 142 | + |
| 143 | + // Extract response data - updated for actual Kapa response structure |
| 144 | + const aiResponse = kapaResponse.data.answer; |
| 145 | + const sources = kapaResponse.data.relevant_sources || []; |
| 146 | + const isUncertain = kapaResponse.data.is_uncertain || false; |
| 147 | + |
| 148 | + if (!aiResponse) { |
| 149 | + throw new Error('No answer received from Kapa AI'); |
| 150 | + } |
| 151 | + |
| 152 | + // Format the response |
| 153 | + let responseBody = `🤖 I've analyzed your question and here's what I found:\n\n`; |
| 154 | + |
| 155 | + // Add uncertainty warning if needed |
| 156 | + if (isUncertain) { |
| 157 | + responseBody += `⚠️ *Note: This response may not be completely accurate. Please verify the information.*\n\n`; |
| 158 | + } |
| 159 | + |
| 160 | + responseBody += `${aiResponse}\n\n`; |
| 161 | + |
| 162 | + if (sources.length > 0) { |
| 163 | + responseBody += `📚 **Relevant documentation:**\n`; |
| 164 | + |
| 165 | + // Process and format sources |
| 166 | + const formattedSources = sources |
| 167 | + .filter(source => source.source_url && source.source_url.startsWith('http') && source.title !== 'Documentation') |
| 168 | + .map(source => { |
| 169 | + const url = source.source_url; |
| 170 | + let title = source.title || 'Documentation'; |
| 171 | + |
| 172 | + // Handle pipe-separated title|subtitle format |
| 173 | + if (title.includes('|')) { |
| 174 | + const parts = title.split('|'); |
| 175 | + const pageTitle = parts[0].trim(); |
| 176 | + const sectionTitle = parts[1].trim(); |
| 177 | + |
| 178 | + // If section title is different from page title, format as "Page - Section" |
| 179 | + if (sectionTitle && sectionTitle !== pageTitle) { |
| 180 | + title = `${pageTitle} - ${sectionTitle}`; |
| 181 | + } else { |
| 182 | + title = pageTitle; |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + return { title, url }; |
| 187 | + }) |
| 188 | + .sort((a, b) => a.title.localeCompare(b.title)); // Sort alphabetically by title |
| 189 | + |
| 190 | + // Remove duplicates (same title and URL) |
| 191 | + const uniqueSources = formattedSources.filter((source, index, array) => |
| 192 | + index === array.findIndex(s => s.title === source.title && s.url === source.url) |
| 193 | + ); |
| 194 | + |
| 195 | + uniqueSources.forEach(source => { |
| 196 | + responseBody += `- [${source.title}](${source.url})\n`; |
| 197 | + }); |
| 198 | + responseBody += `\n`; |
| 199 | + } |
| 200 | + |
| 201 | + responseBody += `---\n\n`; |
| 202 | + responseBody += `ℹ️ This response was generated automatically. `; |
| 203 | + responseBody += `If this doesn't fully answer your question or if you need further assistance, `; |
| 204 | + responseBody += `please mention \`@pwizla\` in a comment and a human maintainer will help you.\n\n`; |
| 205 | + responseBody += `You can also try our [interactive AI chat](https://docs.strapi.io) for more detailed assistance.\n\n`; |
| 206 | + responseBody += `💡 For feature requests or product feedback, visit [feedback.strapi.io](https://feedback.strapi.io).`; |
| 207 | + |
| 208 | + // Post the response |
| 209 | + await github.rest.issues.createComment({ |
| 210 | + owner: context.repo.owner, |
| 211 | + repo: context.repo.repo, |
| 212 | + issue_number: issueNumber, |
| 213 | + body: responseBody |
| 214 | + }); |
| 215 | + |
| 216 | + // Add labels based on enhanced content analysis |
| 217 | + const labels = []; |
| 218 | + const titleLower = issueTitle.toLowerCase(); |
| 219 | + const bodyLower = issueBody.toLowerCase(); |
| 220 | + const combinedContent = `${titleLower} ${bodyLower}`; |
| 221 | + |
| 222 | + // Enhanced label detection |
| 223 | + if (combinedContent.includes('install') || combinedContent.includes('setup') || combinedContent.includes('getting started')) { |
| 224 | + labels.push('installation'); |
| 225 | + } |
| 226 | + if (combinedContent.includes('deploy') || combinedContent.includes('production') || combinedContent.includes('hosting')) { |
| 227 | + labels.push('deployment'); |
| 228 | + } |
| 229 | + if (combinedContent.includes('api') || combinedContent.includes('endpoint') || combinedContent.includes('rest') || combinedContent.includes('graphql')) { |
| 230 | + labels.push('api'); |
| 231 | + } |
| 232 | + if (combinedContent.includes('plugin') || combinedContent.includes('extension')) { |
| 233 | + labels.push('plugins'); |
| 234 | + } |
| 235 | + if (combinedContent.includes('documentation') || combinedContent.includes('docs') || titleLower.includes('[request]')) { |
| 236 | + labels.push('documentation'); |
| 237 | + } |
| 238 | + if (combinedContent.includes('migration') || combinedContent.includes('upgrade') || combinedContent.includes('v4') || combinedContent.includes('v5')) { |
| 239 | + labels.push('migration'); |
| 240 | + } |
| 241 | + if (combinedContent.includes('database') || combinedContent.includes('db') || combinedContent.includes('migration')) { |
| 242 | + labels.push('database'); |
| 243 | + } |
| 244 | + if (combinedContent.includes('admin panel') || combinedContent.includes('admin') || combinedContent.includes('customization')) { |
| 245 | + labels.push('admin-panel'); |
| 246 | + } |
| 247 | + if (combinedContent.includes('auth') || combinedContent.includes('permission') || combinedContent.includes('jwt') || combinedContent.includes('login')) { |
| 248 | + labels.push('authentication'); |
| 249 | + } |
| 250 | + if (combinedContent.includes('i18n') || combinedContent.includes('locale') || combinedContent.includes('translation')) { |
| 251 | + labels.push('i18n'); |
| 252 | + } |
| 253 | + if (combinedContent.includes('typo') || combinedContent.includes('error') || combinedContent.includes('bug') || combinedContent.includes('broken')) { |
| 254 | + labels.push('bug'); |
| 255 | + } |
| 256 | + if (titleLower.includes('[request]') || combinedContent.includes('feature') || combinedContent.includes('enhancement')) { |
| 257 | + labels.push('enhancement'); |
| 258 | + } |
| 259 | + if (titleLower.includes('[auto-sync]') || combinedContent.includes('automatic sync')) { |
| 260 | + labels.push('auto-sync'); |
| 261 | + } |
| 262 | + |
| 263 | + labels.push('auto-responded'); |
| 264 | + |
| 265 | + if (labels.length > 0) { |
| 266 | + await github.rest.issues.addLabels({ |
| 267 | + owner: context.repo.owner, |
| 268 | + repo: context.repo.repo, |
| 269 | + issue_number: issueNumber, |
| 270 | + labels: labels |
| 271 | + }); |
| 272 | + } |
| 273 | + |
| 274 | + console.log(`Successfully processed issue #${issueNumber}`); |
| 275 | + |
| 276 | + } catch (error) { |
| 277 | + console.error('Error calling Kapa AI:', error.message); |
| 278 | + if (error.response) { |
| 279 | + console.error(`HTTP Status: ${error.response.status}`); |
| 280 | + console.error('Response data:', error.response.data); |
| 281 | + } |
| 282 | + |
| 283 | + // Fallback response - Fixed string concatenation |
| 284 | + let fallbackResponse = `👋 Hello @${issueAuthor}! Thanks for opening this issue.\n\n`; |
| 285 | + fallbackResponse += `🤖 I tried to analyze your question automatically but encountered a technical issue. `; |
| 286 | + fallbackResponse += `A human maintainer will review this soon.\n\n`; |
| 287 | + fallbackResponse += `In the meantime, you might find answers in our:\n`; |
| 288 | + fallbackResponse += `- [Documentation](https://docs.strapi.io)\n`; |
| 289 | + fallbackResponse += `- [Community Discord](https://discord.strapi.io)\n`; |
| 290 | + fallbackResponse += `- [Interactive AI chat](https://docs.strapi.io) (click "Ask AI")\n\n`; |
| 291 | + fallbackResponse += `@pwizla please review this issue.`; |
| 292 | + |
| 293 | + await github.rest.issues.createComment({ |
| 294 | + owner: context.repo.owner, |
| 295 | + repo: context.repo.repo, |
| 296 | + issue_number: issueNumber, |
| 297 | + body: fallbackResponse |
| 298 | + }); |
| 299 | + |
| 300 | + await github.rest.issues.addLabels({ |
| 301 | + owner: context.repo.owner, |
| 302 | + repo: context.repo.repo, |
| 303 | + issue_number: issueNumber, |
| 304 | + labels: ['needs-review', 'auto-response-failed'] |
| 305 | + }); |
| 306 | + } |
| 307 | + env: |
| 308 | + KAPA_API_TOKEN: ${{ secrets.KAPA_API_TOKEN }} |
| 309 | + KAPA_PROJECT_ID: ${{ secrets.KAPA_PROJECT_ID }} |
0 commit comments