diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 29f5b186..15e38df8 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -32,6 +32,18 @@ on: required: false type: string default: '' + suggested_response: + required: false + type: string + default: '' + similar_issues: + required: false + type: string + default: '' + workaround: + required: false + type: string + default: '' issue_number: required: true type: string @@ -69,6 +81,9 @@ jobs: INPUT_SUMMARY: ${{ inputs.summary_for_maintainers }} INPUT_CODE_ANALYSIS: ${{ inputs.code_analysis }} INPUT_ENGINEER_GUIDANCE: ${{ inputs.engineer_guidance }} + INPUT_SUGGESTED_RESPONSE: ${{ inputs.suggested_response }} + INPUT_SIMILAR_ISSUES: ${{ inputs.similar_issues }} + INPUT_WORKAROUND: ${{ inputs.workaround }} INPUT_JUSTIFICATION: ${{ inputs.justification }} TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} run: | @@ -142,7 +157,6 @@ jobs: else empty end), (if .implementation_approach then "Implementation Approach: " + (.implementation_approach | @html) else empty end), (if .risks_and_tradeoffs then "Risks & Tradeoffs: " + (.risks_and_tradeoffs | @html) else empty end), - (if .suggested_response then "Suggested Response to User:
" + (.suggested_response | @html) else empty end), (if .related_considerations and (.related_considerations | length) > 0 then "Related Considerations:
" + ([.related_considerations | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value | @html)] | join("
")) else empty end) @@ -152,6 +166,50 @@ jobs: ENGINEER_GUIDANCE="" fi + # Format suggested customer response + SUGGESTED_RESPONSE_RAW="$INPUT_SUGGESTED_RESPONSE" + if [ -n "$SUGGESTED_RESPONSE_RAW" ]; then + SUGGESTED_RESPONSE=$(echo "$SUGGESTED_RESPONSE_RAW" | sed 's/&/\&/g; s//\>/g; s/$/\/g') + else + SUGGESTED_RESPONSE="" + fi + + # Parse and format similar issues analysis + SIMILAR_ISSUES_RAW="$INPUT_SIMILAR_ISSUES" + if [ -n "$SIMILAR_ISSUES_RAW" ]; then + SIMILAR_ISSUES=$(echo "$SIMILAR_ISSUES_RAW" | jq -r ' + [ + (if .summary then "Summary: " + (.summary | @html) else empty end), + (if .duplicate_issues and (.duplicate_issues | length) > 0 + then "Similar/Duplicate Issues:
" + ([.duplicate_issues[] | select(.issue_number | tostring | test("^[0-9]+$")) | "  • #" + (.issue_number | tostring) + " " + (.title | @html) + " [" + (.state | @html) + "] — " + (.similarity | @html) + ": " + (.explanation | @html)] | join("
")) + else empty end), + (if .recently_fixed and (.recently_fixed | length) > 0 + then "Recently Fixed:
" + ([.recently_fixed[] | select(.issue_number | tostring | test("^[0-9]+$")) | "  • #" + (.issue_number | tostring) + " " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("
")) + else empty end) + ] | join("

") + ' 2>/dev/null || echo "$SIMILAR_ISSUES_RAW" | sed 's/&/\&/g; s//\>/g') + else + SIMILAR_ISSUES="" + fi + + # Parse and format workaround analysis + WORKAROUND_RAW="$INPUT_WORKAROUND" + if [ -n "$WORKAROUND_RAW" ]; then + WORKAROUND=$(echo "$WORKAROUND_RAW" | jq -r ' + [ + (if .summary then "Summary: " + (.summary | @html) else empty end), + (if .has_workaround and .workarounds and (.workarounds | length) > 0 + then "Workarounds:
" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value.description | @html) + " [" + (if (.value.confidence | tostring) == "high" or (.value.confidence | tostring) == "medium" or (.value.confidence | tostring) == "low" then (.value.confidence | tostring) else "unknown" end) + " confidence]
    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "
    Code: " + (.value.code_snippet | @html) + "" else "" end)] | join("
")) + else empty end), + (if .can_downgrade == true and .downgrade_version + then "⬇️ Safe to downgrade to: " + (.downgrade_version | @html) + else empty end) + ] | join("

") + ' 2>/dev/null || echo "$WORKAROUND_RAW" | sed 's/&/\&/g; s//\>/g') + else + WORKAROUND="" + fi + # Set severity color indicator case "$SEVERITY" in critical) SEV_INDICATOR="🔴" ;; @@ -176,6 +234,9 @@ jobs: --arg summary "$INPUT_SUMMARY" \ --arg code_analysis "$CODE_ANALYSIS" \ --arg engineer_guidance "$ENGINEER_GUIDANCE" \ + --arg suggested_response "$SUGGESTED_RESPONSE" \ + --arg similar_issues "$SIMILAR_ISSUES" \ + --arg workaround "$WORKAROUND" \ --arg justification "$INPUT_JUSTIFICATION" \ --arg action "$ACTION" \ --arg repo_url "https://github.com/microsoft/mssql-python" \ @@ -198,10 +259,24 @@ jobs: "

" + ($summary | @html) + "

" + "

🔍 Code Analysis

" + "

" + $code_analysis + "

" + + (if $similar_issues != "" then + "

🔄 Similar Issues & Recent Fixes

" + + "

" + $similar_issues + "

" + else "" end) + + (if $workaround != "" then + "

🛠️ Workarounds

" + + "

" + $workaround + "

" + else "" end) + (if $engineer_guidance != "" then "

💡 Engineer Guidance

" + "

" + $engineer_guidance + "

" else "" end) + + (if $suggested_response != "" then + "
" + + "

✉️ Suggested Response to Customer

" + + "

Copy-paste or edit the response below and post it on the issue:

" + + "
" + $suggested_response + "
" + else "" end) + "
" + "

Action Required: " + $action + "

" + "

⚠️ AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.

" + diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index d4e95995..e7d18161 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -228,7 +228,6 @@ jobs: "implementation_approach": "", "effort_estimate": "small|medium|large|epic", "risks_and_tradeoffs": "", - "suggested_response": "", "related_considerations": [""] } @@ -243,9 +242,227 @@ jobs: } } + // --- Search for similar/duplicate issues and recent fixes --- + console.log('Searching for similar issues and recent fixes...'); + let similarIssuesAnalysis = ''; + + try { + // Fetch recent open issues (last 30) + const openIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 30, + sort: 'created', + direction: 'desc' + }); + + // Fetch recently closed issues (last 30) + const closedIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed', + per_page: 30, + sort: 'updated', + direction: 'desc' + }); + + const allIssues = [...openIssues.data, ...closedIssues.data] + .filter(i => i.number !== issueNumber && !i.pull_request) + .map(i => ({ + number: i.number, + title: i.title, + state: i.state, + labels: i.labels.map(l => l.name).join(', '), + created_at: i.created_at, + closed_at: i.closed_at, + body_preview: (i.body || '').slice(0, 300) + })); + + console.log(`Found ${allIssues.length} issues to compare against`); + + const similarPrompt = ` + You are an expert triage system for the mssql-python repository. + A new issue has been filed. Your job is to check if any similar or duplicate issues + already exist (open or recently closed/fixed). + + NEW ISSUE: + Title: ${issueTitle} + Body: ${issueBody.slice(0, 2000)} + Keywords: ${analysis.keywords.join(', ')} + + EXISTING ISSUES (recent open + recently closed): + ${JSON.stringify(allIssues, null, 2).slice(0, 6000)} + + Analyze and respond in JSON: + { + "has_duplicates": true/false, + "has_similar": true/false, + "duplicate_issues": [ + { + "issue_number": , + "title": "", + "state": "open|closed", + "similarity": "duplicate|highly_similar|related", + "explanation": "<why this is a duplicate/similar>" + } + ], + "recently_fixed": [ + { + "issue_number": <number>, + "title": "<title>", + "closed_at": "<date>", + "relevance": "<how this fix relates to the new issue>" + } + ], + "summary": "<1-3 sentence summary: are there duplicates? was this recently fixed? should the new issue be closed as duplicate?>" + } + + IMPORTANT: + - Only flag as duplicate if the core problem is truly the same, not just topically related + - For recently_fixed, only include issues that were closed AND directly address the same problem + - If no duplicates or similar issues exist, set has_duplicates and has_similar to false with empty arrays + `; + + const similarResult = await callGitHubModels(similarPrompt); + similarIssuesAnalysis = similarResult; + const parsed = JSON.parse(similarResult); + console.log(`Similar issues check: duplicates=${parsed.has_duplicates}, similar=${parsed.has_similar}`); + } catch (e) { + console.log(`Similar issues search failed: ${e.message}`); + similarIssuesAnalysis = ''; + } + + // --- Generate workaround suggestions (BUG/BREAK_FIX only) --- + let workaroundAnalysis = ''; + + if (['BUG', 'BREAK_FIX'].includes(analysis.category)) { + console.log('Generating workaround suggestions...'); + + const workaroundContext = codeAnalysis + ? `Code Analysis:\n${codeAnalysis}` + : ''; + + const workaroundPrompt = ` + You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. + A user has reported a ${analysis.category === 'BREAK_FIX' ? 'regression/break-fix' : 'bug'}. + Your job is to suggest practical workarounds the user can apply RIGHT NOW while the team works on a proper fix. + + Issue Title: ${issueTitle} + Issue Body: + ${issueBody.slice(0, 2000)} + + ${workaroundContext} + ${codeContext} + + Provide workaround suggestions in JSON: + { + "has_workaround": true/false, + "workarounds": [ + { + "description": "<clear description of the workaround>", + "code_snippet": "<Python code snippet showing the workaround, if applicable>", + "limitations": "<any limitations or caveats of this workaround>", + "confidence": "high|medium|low" + } + ], + "can_downgrade": true/false, + "downgrade_version": "<safe version to downgrade to, if applicable>", + "summary": "<1-2 sentence summary of available workarounds>" + } + + IMPORTANT: + - Only suggest workarounds you are confident about based on the code and issue description + - Workarounds should be practical and safe for production use + - If no reliable workaround exists, set has_workaround to false + - For BREAK_FIX issues, always consider if downgrading to a previous version is viable + - Code snippets should be complete and copy-pasteable + `; + + try { + workaroundAnalysis = await callGitHubModels(workaroundPrompt); + const parsed = JSON.parse(workaroundAnalysis); + console.log(`Workaround analysis: has_workaround=${parsed.has_workaround}`); + } catch (e) { + console.log(`Workaround generation failed: ${e.message}`); + workaroundAnalysis = ''; + } + } + // NO labels modified on the issue — label info sent to Teams only // NO comment posted to the issue + // --- Generate suggested customer response (all categories) --- + console.log('Generating suggested customer response...'); + let suggestedResponse = ''; + + const analysisContext = codeAnalysis + ? `Code Analysis:\n${codeAnalysis}` + : engineerGuidance + ? `Engineer Guidance:\n${engineerGuidance}` + : ''; + + const workaroundContext = workaroundAnalysis + ? `Workaround Analysis:\n${workaroundAnalysis}` + : ''; + + const similarContext = similarIssuesAnalysis + ? `Similar Issues Analysis:\n${similarIssuesAnalysis}` + : ''; + + const responsePrompt = ` + You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. + A customer filed a GitHub issue and you need to craft a helpful, professional, and empathetic response + that an engineer can copy-paste (or lightly edit) and post on the issue. + + Issue Category: ${analysis.category} + Severity: ${analysis.severity} + Issue Title: ${issueTitle} + Issue Author: @${issueAuthor} + Issue Body: + ${issueBody.slice(0, 3000)} + + ${analysisContext} + + ${workaroundContext} + + ${similarContext} + + Write a suggested response following these guidelines: + - Address the author by their GitHub username (@username) + - Thank them for filing the issue + - Acknowledge the specific problem or request they described + - For BUG/BREAK_FIX: Let them know the team is investigating; ask for OS, Python version, + mssql-python version, SQL Server version, and a minimal repro script if not already provided; + if workarounds are available, mention them briefly + - For FEATURE_REQUEST: Acknowledge the value of the request; mention the team will evaluate it; + ask for use-case details or code examples showing desired behavior if not provided + - For DISCUSSION: Provide helpful guidance or clarification based on the analysis; + point to relevant docs or code if applicable + - If similar or duplicate issues were found, mention them (e.g., "This looks related to #123") + - Always ask for a minimal reproduction script/code snippet if the user hasn't provided one + - Keep the tone warm, professional, and collaborative + - Use Markdown formatting suitable for GitHub comments + - Do NOT promise timelines or specific fixes + - Do NOT reveal internal triage details or AI involvement + - Keep it concise (under 200 words) + + Respond in JSON: + { + "suggested_response": "<the full Markdown response ready to post on GitHub>" + } + `; + + try { + const responseResult = await callGitHubModels(responsePrompt); + const parsed = JSON.parse(responseResult); + suggestedResponse = parsed.suggested_response || ''; + console.log('Suggested customer response generated'); + } catch (e) { + console.log(`Suggested response generation failed: ${e.message}`); + suggestedResponse = ''; + } + // --- Store outputs --- core.setOutput('category', analysis.category); core.setOutput('confidence', analysis.confidence.toString()); @@ -256,6 +473,9 @@ jobs: core.setOutput('keywords', analysis.keywords.join(', ')); core.setOutput('code_analysis', codeAnalysis); core.setOutput('engineer_guidance', engineerGuidance); + core.setOutput('suggested_response', suggestedResponse); + core.setOutput('similar_issues', similarIssuesAnalysis); + core.setOutput('workaround', workaroundAnalysis); core.setOutput('issue_number', issueNumber.toString()); core.setOutput('issue_title', issueTitle); core.setOutput('issue_url', issue.html_url); @@ -271,6 +491,9 @@ jobs: keywords: ${{ steps.triage.outputs.keywords }} code_analysis: ${{ steps.triage.outputs.code_analysis }} engineer_guidance: ${{ steps.triage.outputs.engineer_guidance }} + suggested_response: ${{ steps.triage.outputs.suggested_response }} + similar_issues: ${{ steps.triage.outputs.similar_issues }} + workaround: ${{ steps.triage.outputs.workaround }} issue_number: ${{ steps.triage.outputs.issue_number }} issue_title: ${{ steps.triage.outputs.issue_title }} issue_url: ${{ steps.triage.outputs.issue_url }} @@ -289,6 +512,9 @@ jobs: keywords: ${{ needs.triage.outputs.keywords }} code_analysis: ${{ needs.triage.outputs.code_analysis }} engineer_guidance: ${{ needs.triage.outputs.engineer_guidance }} + suggested_response: ${{ needs.triage.outputs.suggested_response }} + similar_issues: ${{ needs.triage.outputs.similar_issues }} + workaround: ${{ needs.triage.outputs.workaround }} issue_number: ${{ needs.triage.outputs.issue_number }} issue_title: ${{ needs.triage.outputs.issue_title }} issue_url: ${{ needs.triage.outputs.issue_url }} diff --git a/.gitignore b/.gitignore index 5f7378b6..7123641f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ learnings/ # Triage test output files triage-out*.txt triage-err*.txt + +# Local triage test script +test-triage-local.js diff --git a/test-triage-local.js b/test-triage-local.js deleted file mode 100644 index f67eb142..00000000 --- a/test-triage-local.js +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Local test script for issue-triage + issue-notify workflows. - * Tests the exact same logic: fetch issue → GitHub Models classification → Teams notification. - * - * Prerequisites: Node.js 18+ (for native fetch) - * - * Usage: - * $env:GH_TOKEN = "ghp_your_pat_here" # needs models:read scope - * $env:TEAMS_WEBHOOK_URL = "https://your-webhook-url" - * node test-triage-local.js <issue_number> - */ - -const REPO_OWNER = "microsoft"; -const REPO_NAME = "mssql-python"; - -// --- Validate environment --- -const requiredEnv = ["GH_TOKEN", "TEAMS_WEBHOOK_URL"]; -for (const key of requiredEnv) { - if (!process.env[key]) { - console.error(`ERROR: Missing environment variable: ${key}`); - process.exit(1); - } -} - -const issueNumber = parseInt(process.argv[2]); -if (!issueNumber) { - console.error("Usage: node test-triage-local.js <issue_number>"); - process.exit(1); -} - -// --- Helper: GitHub Models --- -async function callGitHubModels(prompt) { - const token = process.env.GH_TOKEN; - const url = "https://models.github.ai/inference/chat/completions"; - - const response = await fetch(url, { - method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "openai/gpt-4.1", - messages: [ - { role: "system", content: "You are an expert assistant. Always respond in valid json format." }, - { role: "user", content: prompt }, - ], - temperature: 0.1, - response_format: { type: "json_object" }, - }), - }); - - if (!response.ok) { - const errText = await response.text(); - throw new Error(`GitHub Models error: ${response.status} - ${errText}`); - } - - const data = await response.json(); - return data.choices[0].message.content; -} - -// --- Helper: Fetch issue from GitHub --- -async function fetchIssue(issueNum) { - const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNum}`; - const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; - - // Use GH_TOKEN if available for higher rate limits - if (process.env.GH_TOKEN) { - headers["Authorization"] = `token ${process.env.GH_TOKEN}`; - } - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} - ${await response.text()}`); - } - return response.json(); -} - -// --- Helper: Fetch file content from GitHub --- -async function fetchFileContent(filePath) { - const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/${filePath}`; - const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; - - if (process.env.GH_TOKEN) { - headers["Authorization"] = `token ${process.env.GH_TOKEN}`; - } - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`Could not fetch ${filePath}: ${response.status}`); - } - const data = await response.json(); - return Buffer.from(data.content, "base64").toString(); -} - -// --- Helper: Send Teams notification --- -async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, issue) { - const category = analysis.category; - const severity = analysis.severity; - - let emoji, categoryDisplay, action; - switch (category) { - case "FEATURE_REQUEST": - emoji = "💡"; categoryDisplay = "Feature Request"; - action = "Evaluate against roadmap. If approved, create ADO work item."; - break; - case "BUG": - emoji = "🐛"; categoryDisplay = "Bug"; - action = "Validate bug, reproduce if possible, assign to developer."; - break; - case "DISCUSSION": - emoji = "💬"; categoryDisplay = "Discussion"; - action = "Respond with guidance. Re-classify if needed."; - break; - case "BREAK_FIX": - emoji = "🚨"; categoryDisplay = "Break/Fix (Regression)"; - action = "URGENT: Assign to senior dev, create P0/P1 ADO item."; - break; - default: - emoji = "❓"; categoryDisplay = "Unknown"; - action = "Review and manually classify this issue."; - } - - const sevIndicator = severity === "critical" ? "🔴" - : severity === "high" ? "🟠" - : severity === "medium" ? "🟡" : "🟢"; - - const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - let codeAnalysisText = "N/A — classification did not require code analysis."; - let engineerGuidanceText = ""; - - if (codeAnalysis) { - try { - const parsed = JSON.parse(codeAnalysis); - - // Format code analysis as HTML with bold labels - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.is_bug) parts.push(`<b>Verdict:</b> ${escVal(parsed.is_bug)}`); - if (parsed.root_cause) parts.push(`<b>Root Cause:</b> ${escVal(parsed.root_cause)}`); - if (parsed.affected_components && parsed.affected_components.length > 0) { - parts.push(`<b>Affected Components:</b><br>${parsed.affected_components.map(c => `  • ${escVal(c)}`).join("<br>")}`); - } - if (parsed.evidence_and_context) parts.push(`<b>Evidence & Context:</b> ${escVal(parsed.evidence_and_context)}`); - if (parsed.recommended_fixes && parsed.recommended_fixes.length > 0) { - parts.push(`<b>Recommended Fixes:</b><br>${parsed.recommended_fixes.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("<br>")}`); - } - if (parsed.code_locations && parsed.code_locations.length > 0) { - parts.push(`<b>Code Locations:</b><br>${parsed.code_locations.map(l => `  • ${escVal(l)}`).join("<br>")}`); - } - if (parsed.risk_assessment) parts.push(`<b>Risk Assessment:</b> ${escVal(parsed.risk_assessment)}`); - codeAnalysisText = parts.join("<br><br>"); - } catch (e) { - codeAnalysisText = esc(codeAnalysis); - if (codeAnalysisText.length > 3000) { - codeAnalysisText = codeAnalysisText.slice(0, 3000) + "... (truncated)"; - } - } - } - - if (engineerGuidance) { - try { - const parsed = JSON.parse(engineerGuidance); - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.technical_assessment) parts.push(`<b>Technical Assessment:</b> ${escVal(parsed.technical_assessment)}`); - if (parsed.verdict) parts.push(`<b>Verdict:</b> ${escVal(parsed.verdict)}`); - if (parsed.effort_estimate) parts.push(`Effort Estimate: ${escVal(parsed.effort_estimate)}`); - if (parsed.affected_files && parsed.affected_files.length > 0) { - parts.push(`<b>Affected Files:</b><br>${parsed.affected_files.map(a => `  • ${escVal(a)}`).join("<br>")}`); - } - if (parsed.implementation_approach) parts.push(`<b>Implementation Approach:</b> ${escVal(parsed.implementation_approach)}`); - if (parsed.risks_and_tradeoffs) parts.push(`<b>Risks & Tradeoffs:</b> ${escVal(parsed.risks_and_tradeoffs)}`); - if (parsed.suggested_response) parts.push(`<b>Suggested Response to User:</b><br>${escVal(parsed.suggested_response)}`); - if (parsed.related_considerations && parsed.related_considerations.length > 0) { - parts.push(`<b>Related Considerations:</b><br>${parsed.related_considerations.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("<br>")}`); - } - engineerGuidanceText = parts.join("<br><br>"); - } catch (e) { - engineerGuidanceText = esc(engineerGuidance); - if (engineerGuidanceText.length > 3000) { - engineerGuidanceText = engineerGuidanceText.slice(0, 3000) + "... (truncated)"; - } - } - } - - const htmlMessage = [ - `<h2>${emoji} mssql-python Issue Triage</h2>`, - `<p><b>${esc(categoryDisplay)}</b>  |  `, - `${sevIndicator} Severity: <b>${esc(severity)}</b>  |  `, - `Confidence: <b>${analysis.confidence}%</b></p>`, - `<hr>`, - `<p>`, - `📌 <b>Issue:</b> <a href="${issue.html_url}">#${issue.number} — ${esc(issue.title)}</a><br>`, - `👤 <b>Author:</b> @${esc(issue.user.login)}<br>`, - `🏷️ <b>Keywords:</b> ${esc(analysis.keywords.join(", "))}<br>`, - `📂 <b>Relevant Files:</b> ${esc(analysis.relevant_source_files.join(", "))}`, - `</p>`, - `<hr>`, - `<h3>📝 Analysis</h3>`, - `<p>${esc(analysis.summary_for_maintainers)}</p>`, - `<h3>🔍 Code Analysis</h3>`, - `<p>${codeAnalysisText}</p>`, - engineerGuidanceText ? `<h3>💡 Engineer Guidance</h3>` : '', - engineerGuidanceText ? `<p>${engineerGuidanceText}</p>` : '', - `<hr>`, - `<p>⚡ <b>Action Required:</b> ${esc(action)}</p>`, - `<p><i>⚠️ AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.</i></p>`, - `<p><a href="${issue.html_url}">📋 View Issue</a>`, - `  |  `, - `<a href="https://github.com/${REPO_OWNER}/${REPO_NAME}">📂 View Repository</a></p>`, - ].join(""); - - const payload = { text: htmlMessage }; - - const response = await fetch(process.env.TEAMS_WEBHOOK_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Teams webhook error: ${response.status} - ${text}`); - } - - return response.status; -} - -// --- Main --- -async function main() { - console.log(`\n🔍 Fetching issue #${issueNumber}...`); - const issue = await fetchIssue(issueNumber); - console.log(` Title: ${issue.title}`); - console.log(` Author: ${issue.user.login}`); - - console.log(`\n🤖 Classifying with GitHub Models...`); - const classificationPrompt = ` -You are an expert triage system for the mssql-python repository — a Python driver for Microsoft SQL Server. -The driver uses ODBC under the hood with a C++/pybind11 native extension layer and Python wrappers. -Note: The pybind/ directory contains C++/pybind11 code (NOT Rust). Only reference Rust if the issue is specifically about BCP (Bulk Copy Protocol). - -Key source files in the repo: -- mssql_python/connection.py — Connection management, pooling integration -- mssql_python/cursor.py — Cursor operations, execute, fetch, bulkcopy -- mssql_python/auth.py — Authentication (SQL auth, Azure AD, etc.) -- mssql_python/exceptions.py — Error handling and exception classes -- mssql_python/pooling.py — Connection pooling -- mssql_python/helpers.py — Utility functions -- mssql_python/constants.py — Constants, SQL types, enums -- mssql_python/connection_string_parser.py — Connection string parsing -- mssql_python/parameter_helper.py — Query parameter handling -- mssql_python/logging.py — Logging infrastructure -- mssql_python/row.py — Row objects -- mssql_python/type.py — Type mappings -- mssql_python/ddbc_bindings.py — Python/pybind11 ODBC bindings (C++ native extension, NOT Rust) -- mssql_python/pybind/ — C++/pybind11 native extension layer (NOT Rust) - -Classify the following GitHub issue into EXACTLY ONE category: - -1. FEATURE_REQUEST — User wants new functionality or enhancements -2. BUG — Something is broken, incorrect behavior, or errors -3. DISCUSSION — User is asking a question or wants clarification -4. BREAK_FIX — A regression or critical bug: segfaults, crashes, data corruption, - or user says "this used to work" - -Respond in this exact JSON format: -{ - "category": "BUG|FEATURE_REQUEST|DISCUSSION|BREAK_FIX", - "confidence": <0-100>, - "justification": "<2-3 sentence explanation>", - "severity": "critical|high|medium|low", - "relevant_source_files": ["<top 3 most relevant source file paths>"], - "keywords": ["<key technical terms from the issue>"], - "summary_for_maintainers": "<detailed 3-5 sentence analysis for maintainer notification>" -} - -Issue Title: ${issue.title} -Issue Body: -${(issue.body || "").slice(0, 4000)} -`; - - const classifyResult = await callGitHubModels(classificationPrompt); - const analysis = JSON.parse(classifyResult); - - console.log(`\n📊 Classification Results:`); - console.log(` Category: ${analysis.category}`); - console.log(` Confidence: ${analysis.confidence}%`); - console.log(` Severity: ${analysis.severity}`); - console.log(` Keywords: ${analysis.keywords.join(", ")}`); - console.log(` Files: ${analysis.relevant_source_files.join(", ")}`); - console.log(` Summary: ${analysis.summary_for_maintainers}`); - - // --- Fetch relevant source files (for ALL categories) --- - console.log(`\n📂 Fetching relevant source files for code-grounded analysis...`); - const fileContents = []; - for (const filePath of analysis.relevant_source_files.slice(0, 3)) { - try { - const content = await fetchFileContent(filePath); - fileContents.push(`### File: ${filePath}\n\`\`\`python\n${content.slice(0, 3000)}\n\`\`\``); - console.log(` ✅ Fetched ${filePath}`); - } catch (e) { - console.log(` ⚠️ Could not fetch ${filePath}: ${e.message}`); - } - } - - const codeContext = fileContents.length > 0 - ? `\n\nRelevant source files from the repository:\n${fileContents.join("\n\n")}` - : ''; - - // --- For BUG/BREAK_FIX, analyze codebase --- - let codeAnalysis = ""; - - if (["BUG", "BREAK_FIX"].includes(analysis.category) && fileContents.length > 0) { - console.log(`\n🔬 Bug/Break-fix detected — analyzing codebase...`); - - const codePrompt = ` -You are a senior Python developer analyzing a potential -${analysis.category === "BREAK_FIX" ? "regression/break-fix" : "bug"} -in the mssql-python driver (Python + ODBC + C++/pybind11 native layer). -IMPORTANT: ddbc_bindings.py and the pybind/ directory are C++/pybind11 code, NOT Rust. Only mention Rust if the issue is specifically about BCP (Bulk Copy Protocol). -IMPORTANT: Base your analysis ONLY on the actual source code provided below. Do not speculate about code you haven't seen. - -Bug Report: -Title: ${issue.title} -Body: ${(issue.body || "").slice(0, 2000)} -${codeContext} - -Provide analysis in JSON: -{ - "is_bug": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", - "root_cause": "<detailed root cause analysis based on actual code above>", - "affected_components": ["<affected modules/functions from the code above>"], - "evidence_and_context": "<specific evidence from the codebase — cite exact functions, variables, line logic, or patterns that support your analysis>", - "recommended_fixes": ["<fix 1 — describe the approach referencing specific code>", "<fix 2>", "<fix 3>"], - "code_locations": ["<file:function or file:class where changes should be made>"], - "risk_assessment": "<risk to users>" -} -`; - - try { - codeAnalysis = await callGitHubModels(codePrompt); - const parsed = JSON.parse(codeAnalysis); - console.log(`\n🔍 Code Analysis:`); - console.log(` Is Bug: ${parsed.is_bug}`); - console.log(` Root Cause: ${parsed.root_cause}`); - if (parsed.evidence_and_context) { - console.log(` Evidence: ${parsed.evidence_and_context}`); - } - if (parsed.recommended_fixes && parsed.recommended_fixes.length > 0) { - console.log(`\n\ud83d\udee0\ufe0f Recommended Fixes:`); - for (const fix of parsed.recommended_fixes) { - console.log(` \u2022 ${fix}`); - } - } - if (parsed.code_locations && parsed.code_locations.length > 0) { - console.log(`\n\ud83d\udccd Code Locations:`); - for (const loc of parsed.code_locations) { - console.log(` \u2022 ${loc}`); - } - } - } catch (e) { - console.log(` ⚠️ Code analysis failed: ${e.message}`); - } - } - - // --- For FEATURE_REQUEST/DISCUSSION, provide code-grounded engineer guidance --- - let engineerGuidance = ""; - - if (["FEATURE_REQUEST", "DISCUSSION"].includes(analysis.category)) { - console.log(`\n💡 Non-bug issue — generating code-grounded engineer guidance...`); - - const guidancePrompt = ` -You are a senior engineer on the mssql-python team — a Python driver for Microsoft SQL Server -(ODBC + C++/pybind11 native extension + Python wrappers). -IMPORTANT: Base your analysis ONLY on the actual source code provided below. Do not speculate about code you haven't seen. If the code doesn't contain enough information, say so explicitly. - -A user filed a GitHub issue classified as: ${analysis.category} - -Issue Title: ${issue.title} -Issue Body: -${(issue.body || "").slice(0, 3000)} -${codeContext} - -Based on the ACTUAL SOURCE CODE above, provide a detailed analysis to help the engineering team respond efficiently. -Respond in JSON: -{ - "technical_assessment": "<detailed technical assessment grounded in the actual code above>", - "verdict": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", - "issue_identified": true/false, - "affected_files": ["<specific source files, modules, functions, or classes from the code above>"], - "current_behavior": "<describe what the current code actually does based on your reading>", - "implementation_approach": "<concrete implementation steps referencing specific functions/lines from the code — ONLY if issue_identified is true, otherwise empty string>", - "effort_estimate": "small|medium|large|epic", - "risks_and_tradeoffs": "<potential risks, backward compatibility concerns, or tradeoffs — ONLY if issue_identified is true, otherwise empty string>", - "suggested_response": "<a draft response the engineer could post on the issue. Always ask the user to share a minimal repro or code snippet that demonstrates the issue or desired behavior, if they haven't already provided one.>", - "related_considerations": ["<other things the team should think about — ONLY if issue_identified is true, otherwise empty array>"] -} - -IMPORTANT: If your technical_assessment does not identify any actual issue or gap in the code, set issue_identified to false and leave implementation_approach, risks_and_tradeoffs, and related_considerations empty. Only populate those fields when a real problem or improvement opportunity is confirmed in the code. -`; - - try { - engineerGuidance = await callGitHubModels(guidancePrompt); - const parsed = JSON.parse(engineerGuidance); - console.log(`\n💡 Engineer Guidance:`); - console.log(` Verdict: ${parsed.verdict}`); - console.log(` Effort: ${parsed.effort_estimate}`); - console.log(` Current Code: ${parsed.current_behavior}`); - console.log(` Assessment: ${parsed.technical_assessment}`); - console.log(` Approach: ${parsed.implementation_approach}`); - console.log(` Risks: ${parsed.risks_and_tradeoffs}`); - } catch (e) { - console.log(` ⚠️ Engineer guidance failed: ${e.message}`); - } - } - - // --- Send Teams notification --- - console.log(`\n📤 Sending Teams notification...`); - try { - const status = await sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, issue); - console.log(` ✅ Teams notification sent (HTTP ${status})`); - } catch (e) { - console.error(` ❌ Teams notification failed: ${e.message}`); - } - - console.log(`\n✅ Triage complete for issue #${issueNumber}`); -} - -main().catch((e) => { - console.error(`\n❌ Fatal error: ${e.message}`); - process.exit(1); -});