From d7e7e40bbec5017c75357ed54db59dcfa34fa254 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Thu, 19 Feb 2026 01:16:43 +0530 Subject: [PATCH 1/6] Add issue triage and Teams notification workflows --- .github/workflows/issue-notify.yml | 265 +++++++++++++++++++++++++++++ .github/workflows/issue-triage.yml | 246 ++++++++++++++++++++++++++ 2 files changed, 511 insertions(+) create mode 100644 .github/workflows/issue-notify.yml create mode 100644 .github/workflows/issue-triage.yml diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml new file mode 100644 index 00000000..7d48d8d2 --- /dev/null +++ b/.github/workflows/issue-notify.yml @@ -0,0 +1,265 @@ +name: Issue Notification + +on: + workflow_call: + inputs: + category: + required: true + type: string + confidence: + required: true + type: string + severity: + required: true + type: string + justification: + required: true + type: string + summary_for_maintainers: + required: true + type: string + relevant_files: + required: true + type: string + keywords: + required: true + type: string + code_analysis: + required: false + type: string + default: '' + issue_number: + required: true + type: string + issue_title: + required: true + type: string + issue_url: + required: true + type: string + issue_author: + required: true + type: string + secrets: + TEAMS_WEBHOOK_URL: + required: true + +jobs: + send-notification: + runs-on: ubuntu-latest + steps: + - name: Send Teams Channel notification + env: + INPUT_CATEGORY: ${{ inputs.category }} + INPUT_SEVERITY: ${{ inputs.severity }} + INPUT_CONFIDENCE: ${{ inputs.confidence }} + INPUT_ISSUE_NUMBER: ${{ inputs.issue_number }} + INPUT_ISSUE_TITLE: ${{ inputs.issue_title }} + INPUT_ISSUE_AUTHOR: ${{ inputs.issue_author }} + INPUT_ISSUE_URL: ${{ inputs.issue_url }} + INPUT_KEYWORDS: ${{ inputs.keywords }} + INPUT_RELEVANT_FILES: ${{ inputs.relevant_files }} + INPUT_SUMMARY: ${{ inputs.summary_for_maintainers }} + INPUT_CODE_ANALYSIS: ${{ inputs.code_analysis }} + INPUT_ACTION_TEXT: ${{ inputs.justification }} + TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} + run: | + CATEGORY="$INPUT_CATEGORY" + SEVERITY="$INPUT_SEVERITY" + + # Set emoji and action based on category + case "$CATEGORY" in + FEATURE_REQUEST) + EMOJI="šŸ’”" + CATEGORY_DISPLAY="Feature Request" + ACTION="Evaluate against roadmap. If approved, create ADO work item." + ;; + BUG) + EMOJI="šŸ›" + CATEGORY_DISPLAY="Bug" + ACTION="Validate bug, reproduce if possible, assign to developer." + ;; + DISCUSSION) + EMOJI="šŸ’¬" + CATEGORY_DISPLAY="Discussion" + ACTION="Respond with guidance. Re-classify if needed." + ;; + BREAK_FIX) + EMOJI="🚨" + CATEGORY_DISPLAY="Break/Fix (Regression)" + ACTION="URGENT: Assign to senior dev, create P0/P1 ADO item." + ;; + *) + EMOJI="ā“" + CATEGORY_DISPLAY="Unknown" + ACTION="Review and manually classify this issue." + ;; + esac + + # Truncate code analysis if too long + CODE_ANALYSIS="$INPUT_CODE_ANALYSIS" + if [ ${#CODE_ANALYSIS} -gt 1500 ]; then + CODE_ANALYSIS="${CODE_ANALYSIS:0:1500}... (see Actions log for full analysis)" + fi + if [ -z "$CODE_ANALYSIS" ]; then + CODE_ANALYSIS="N/A — classification did not require code analysis." + fi + + # Build Adaptive Card payload using jq for proper JSON escaping + jq -n \ + --arg emoji "$EMOJI" \ + --arg category "$CATEGORY" \ + --arg category_display "$CATEGORY_DISPLAY" \ + --arg severity "$SEVERITY" \ + --arg confidence "$INPUT_CONFIDENCE" \ + --arg issue_num "$INPUT_ISSUE_NUMBER" \ + --arg issue_title "$INPUT_ISSUE_TITLE" \ + --arg issue_author "$INPUT_ISSUE_AUTHOR" \ + --arg issue_url "$INPUT_ISSUE_URL" \ + --arg keywords "$INPUT_KEYWORDS" \ + --arg relevant_files "$INPUT_RELEVANT_FILES" \ + --arg summary "$INPUT_SUMMARY" \ + --arg code_analysis "$CODE_ANALYSIS" \ + --arg action "$ACTION" \ + '{ + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "text": ($emoji + " mssql-python Issue Triage"), + "wrap": true, + "style": "heading" + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": $category_display, + "weight": "bolder", + "color": (if $category == "BREAK_FIX" then "attention" + elif $category == "BUG" then "warning" + else "default" end), + "size": "medium" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": ("Severity: " + $severity), + "size": "medium", + "color": (if $severity == "critical" then "attention" + elif $severity == "high" then "warning" + else "default" end) + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": ("Confidence: " + $confidence + "%"), + "size": "medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "separator": true, + "facts": [ + { "title": "Issue", "value": ("#" + $issue_num + " — " + $issue_title) }, + { "title": "Author", "value": ("@" + $issue_author) }, + { "title": "Keywords", "value": $keywords }, + { "title": "Relevant Files", "value": $relevant_files } + ] + }, + { + "type": "TextBlock", + "text": "**šŸ“ Analysis**", + "weight": "bolder", + "spacing": "medium", + "wrap": true + }, + { + "type": "TextBlock", + "text": $summary, + "wrap": true + }, + { + "type": "TextBlock", + "text": "**šŸ” Code Analysis**", + "weight": "bolder", + "spacing": "medium", + "wrap": true + }, + { + "type": "TextBlock", + "text": $code_analysis, + "wrap": true, + "fontType": "monospace", + "size": "small" + }, + { + "type": "TextBlock", + "text": ("⚔ **Action Required:** " + $action), + "weight": "bolder", + "color": "attention", + "spacing": "medium", + "wrap": true, + "separator": true + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "šŸ“‹ View Issue", + "url": $issue_url + }, + { + "type": "Action.OpenUrl", + "title": "šŸ“‚ View Repository", + "url": "https://github.com/microsoft/mssql-python" + } + ] + } + } + ] + }' > /tmp/teams_payload.json + + echo "Sending notification to Teams Channel..." + + HTTP_STATUS=$(curl -s -o /tmp/teams_response.txt -w "%{http_code}" \ + -H "Content-Type: application/json" \ + -d @/tmp/teams_payload.json \ + "$TEAMS_WEBHOOK_URL") + + echo "Teams API response: $HTTP_STATUS" + cat /tmp/teams_response.txt + + if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then + echo "::error::Failed to send Teams notification. HTTP status: $HTTP_STATUS" + exit 1 + fi + + echo "āœ… Teams Channel notification sent successfully" \ No newline at end of file diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 00000000..f41327d3 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,246 @@ +name: Issue Triage + +on: + issues: + types: [opened] + + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to triage' + required: true + type: number + +permissions: + issues: read + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Wait for acknowledgement + run: sleep 120 + + - name: Triage Analysis + id: triage + uses: actions/github-script@v7 + env: + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} + AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // --- Helper function for Azure OpenAI --- + async function callAzureOpenAI(prompt) { + const endpoint = process.env.AZURE_OPENAI_ENDPOINT; + const apiKey = process.env.AZURE_OPENAI_KEY; + const deployment = process.env.AZURE_OPENAI_DEPLOYMENT; + + const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=2025-01-01-preview`; + + const response = await fetch(url, { + method: "POST", + headers: { + "api-key": apiKey, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + messages: [{ role: "user", content: prompt }], + temperature: 0.1, + response_format: { type: "json_object" } + }) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Azure OpenAI error: ${response.status} - ${errText}`); + } + + const data = await response.json(); + return data.choices[0].message.content; + } + + // --- Get issue details --- + const issueNumber = context.payload.inputs?.issue_number + ? parseInt(context.payload.inputs.issue_number) + : context.payload.issue.number; + + let issue; + if (context.payload.issue && !context.payload.inputs?.issue_number) { + issue = context.payload.issue; + } else { + issue = (await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + })).data; + } + + const issueTitle = issue.title; + const issueBody = issue.body || ''; + const issueAuthor = issue.user.login; + + console.log(`Triaging issue #${issueNumber}: ${issueTitle}`); + + // --- Classify the issue --- + 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 Rust-based core (pybind) and Python wrappers. + + 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 — ODBC bindings + - mssql_python/pybind/ — Rust core bindings + + 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": [""], + "keywords": [""], + "summary_for_maintainers": "" + } + + Issue Title: ${issueTitle} + Issue Body: + ${issueBody.slice(0, 4000)} + `; + + let analysis; + try { + const classifyResult = await callAzureOpenAI(classificationPrompt); + analysis = JSON.parse(classifyResult); + } catch (e) { + core.setFailed(`Classification failed: ${e.message}`); + return; + } + + console.log(`Classification: ${analysis.category} (${analysis.confidence}%)`); + console.log(`Severity: ${analysis.severity}`); + + // --- For BUG/BREAK_FIX, analyze codebase --- + let codeAnalysis = ''; + + if (['BUG', 'BREAK_FIX'].includes(analysis.category)) { + console.log('Bug/Break-fix detected — analyzing codebase...'); + + const fileContents = []; + for (const filePath of analysis.relevant_source_files.slice(0, 3)) { + try { + const file = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: filePath + }); + const content = Buffer.from(file.data.content, 'base64').toString(); + fileContents.push(`### File: ${filePath}\n\`\`\`python\n${content.slice(0, 3000)}\n\`\`\``); + } catch (e) { + console.log(`Could not fetch ${filePath}: ${e.message}`); + } + } + + if (fileContents.length > 0) { + 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 + Rust core). + + Bug Report: + Title: ${issueTitle} + Body: ${issueBody.slice(0, 2000)} + + Relevant source files: + ${fileContents.join('\n\n')} + + Provide analysis in JSON: + { + "is_bug": "confirmed|likely|uncertain|not_a_bug", + "root_cause": "", + "affected_components": [""], + "suggested_fix": "", + "risk_assessment": "", + "recommended_solutions": ["", "", ""] + } + `; + + try { + codeAnalysis = await callAzureOpenAI(codePrompt); + console.log('Code analysis complete'); + } catch (e) { + console.log(`Code analysis failed: ${e.message}`); + } + } + } + + // NO labels modified on the issue — label info sent to Teams only + // NO comment posted to the issue + + // --- Store outputs --- + core.setOutput('category', analysis.category); + core.setOutput('confidence', analysis.confidence.toString()); + core.setOutput('severity', analysis.severity); + core.setOutput('justification', analysis.justification); + core.setOutput('summary_for_maintainers', analysis.summary_for_maintainers || analysis.justification); + core.setOutput('relevant_files', analysis.relevant_source_files.join(', ')); + core.setOutput('keywords', analysis.keywords.join(', ')); + core.setOutput('code_analysis', codeAnalysis); + core.setOutput('issue_number', issueNumber.toString()); + core.setOutput('issue_title', issueTitle); + core.setOutput('issue_url', issue.html_url); + core.setOutput('issue_author', issueAuthor); + + outputs: + category: ${{ steps.triage.outputs.category }} + confidence: ${{ steps.triage.outputs.confidence }} + severity: ${{ steps.triage.outputs.severity }} + justification: ${{ steps.triage.outputs.justification }} + summary_for_maintainers: ${{ steps.triage.outputs.summary_for_maintainers }} + relevant_files: ${{ steps.triage.outputs.relevant_files }} + keywords: ${{ steps.triage.outputs.keywords }} + code_analysis: ${{ steps.triage.outputs.code_analysis }} + issue_number: ${{ steps.triage.outputs.issue_number }} + issue_title: ${{ steps.triage.outputs.issue_title }} + issue_url: ${{ steps.triage.outputs.issue_url }} + issue_author: ${{ steps.triage.outputs.issue_author }} + + notify: + needs: triage + uses: ./.github/workflows/issue-notify.yml + with: + category: ${{ needs.triage.outputs.category }} + confidence: ${{ needs.triage.outputs.confidence }} + severity: ${{ needs.triage.outputs.severity }} + justification: ${{ needs.triage.outputs.justification }} + summary_for_maintainers: ${{ needs.triage.outputs.summary_for_maintainers }} + relevant_files: ${{ needs.triage.outputs.relevant_files }} + keywords: ${{ needs.triage.outputs.keywords }} + code_analysis: ${{ needs.triage.outputs.code_analysis }} + issue_number: ${{ needs.triage.outputs.issue_number }} + issue_title: ${{ needs.triage.outputs.issue_title }} + issue_url: ${{ needs.triage.outputs.issue_url }} + issue_author: ${{ needs.triage.outputs.issue_author }} + secrets: + TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} \ No newline at end of file From 741987e599a3102925709d8c95b79a7c3c7c4113 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 18 Mar 2026 01:09:54 +0530 Subject: [PATCH 2/6] FEAT: AI-powered issue triage with GitHub Models - Add code-grounded analysis: fetch source files before AI calls for accuracy - Add engineer guidance for non-bug issues (FEATURE_REQUEST/DISCUSSION) - Replace speculative code changes with evidence-based sections - Add verdict labels (Confirmed Bug/Likely Bug/Require More Analysis/Not a Bug) - Format Teams notifications with bold labels and proper HTML rendering - Add local test script (test-triage-local.js) for development - Update .gitignore for triage test output files --- .github/workflows/issue-notify.yml | 221 ++++++--------- .github/workflows/issue-triage.yml | 171 ++++++++---- .gitignore | 4 + test-triage-local.js | 435 +++++++++++++++++++++++++++++ 4 files changed, 639 insertions(+), 192 deletions(-) create mode 100644 test-triage-local.js diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 7d48d8d2..d89e8507 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -28,6 +28,10 @@ on: required: false type: string default: '' + engineer_guidance: + required: false + type: string + default: '' issue_number: required: true type: string @@ -61,6 +65,7 @@ jobs: INPUT_RELEVANT_FILES: ${{ inputs.relevant_files }} INPUT_SUMMARY: ${{ inputs.summary_for_maintainers }} INPUT_CODE_ANALYSIS: ${{ inputs.code_analysis }} + INPUT_ENGINEER_GUIDANCE: ${{ inputs.engineer_guidance }} INPUT_ACTION_TEXT: ${{ inputs.justification }} TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} run: | @@ -96,21 +101,68 @@ jobs: ;; esac - # Truncate code analysis if too long - CODE_ANALYSIS="$INPUT_CODE_ANALYSIS" - if [ ${#CODE_ANALYSIS} -gt 1500 ]; then - CODE_ANALYSIS="${CODE_ANALYSIS:0:1500}... (see Actions log for full analysis)" - fi - if [ -z "$CODE_ANALYSIS" ]; then + # Parse and format code analysis from JSON into readable text + CODE_ANALYSIS_RAW="$INPUT_CODE_ANALYSIS" + if [ -n "$CODE_ANALYSIS_RAW" ]; then + # Try to parse as JSON and extract structured fields + CODE_ANALYSIS=$(echo "$CODE_ANALYSIS_RAW" | jq -r ' + [ + (if .is_bug then "Verdict: " + .is_bug else empty end), + (if .root_cause then "\nRoot Cause: " + .root_cause else empty end), + (if .affected_components and (.affected_components | length) > 0 + then "\nAffected Components:\n" + ([.affected_components[] | " • " + .] | join("\n")) + else empty end), + (if .evidence_and_context then "\nEvidence & Context: " + .evidence_and_context else empty end), + (if .recommended_fixes and (.recommended_fixes | length) > 0 + then "\nRecommended Fixes:\n" + ([.recommended_fixes | to_entries[] | " " + ((.key + 1) | tostring) + ". " + .value] | join("\n")) + else empty end), + (if .code_locations and (.code_locations | length) > 0 + then "\nCode Locations:\n" + ([.code_locations[] | " • " + .] | join("\n")) + else empty end), + (if .risk_assessment then "\nRisk Assessment: " + .risk_assessment else empty end) + ] | join("\n") + ' 2>/dev/null || echo "$CODE_ANALYSIS_RAW") + else CODE_ANALYSIS="N/A — classification did not require code analysis." fi - # Build Adaptive Card payload using jq for proper JSON escaping + # Parse and format engineer guidance from JSON into readable text + ENGINEER_GUIDANCE_RAW="$INPUT_ENGINEER_GUIDANCE" + if [ -n "$ENGINEER_GUIDANCE_RAW" ]; then + ENGINEER_GUIDANCE=$(echo "$ENGINEER_GUIDANCE_RAW" | jq -r ' + [ + (if .technical_assessment then "Technical Assessment: " + .technical_assessment else empty end), + (if .verdict then "Verdict: " + .verdict else empty end), + (if .effort_estimate then "Effort Estimate: " + .effort_estimate else empty end), + (if .affected_files and (.affected_files | length) > 0 + then "Affected Files:
" + ([.affected_files[] | "  ā€¢ " + .] | join("
")) + else empty end), + (if .implementation_approach then "Implementation Approach: " + .implementation_approach else empty end), + (if .risks_and_tradeoffs then "Risks & Tradeoffs: " + .risks_and_tradeoffs else empty end), + (if .suggested_response then "Suggested Response to User:
" + .suggested_response else empty end), + (if .related_considerations and (.related_considerations | length) > 0 + then "Related Considerations:
" + ([.related_considerations | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + .value] | join("
")) + else empty end) + ] | join("

") + ' 2>/dev/null || echo "$ENGINEER_GUIDANCE_RAW") + else + ENGINEER_GUIDANCE="" + fi + + # Set severity color indicator + case "$SEVERITY" in + critical) SEV_INDICATOR="šŸ”“" ;; + high) SEV_INDICATOR="🟠" ;; + medium) SEV_INDICATOR="🟔" ;; + *) SEV_INDICATOR="🟢" ;; + esac + + # Build well-formatted HTML message using jq for proper JSON escaping jq -n \ --arg emoji "$EMOJI" \ - --arg category "$CATEGORY" \ --arg category_display "$CATEGORY_DISPLAY" \ --arg severity "$SEVERITY" \ + --arg sev_indicator "$SEV_INDICATOR" \ --arg confidence "$INPUT_CONFIDENCE" \ --arg issue_num "$INPUT_ISSUE_NUMBER" \ --arg issue_title "$INPUT_ISSUE_TITLE" \ @@ -120,131 +172,38 @@ jobs: --arg relevant_files "$INPUT_RELEVANT_FILES" \ --arg summary "$INPUT_SUMMARY" \ --arg code_analysis "$CODE_ANALYSIS" \ + --arg engineer_guidance "$ENGINEER_GUIDANCE" \ --arg action "$ACTION" \ + --arg repo_url "https://github.com/microsoft/mssql-python" \ '{ - "type": "message", - "attachments": [ - { - "contentType": "application/vnd.microsoft.card.adaptive", - "content": { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.4", - "body": [ - { - "type": "TextBlock", - "size": "large", - "weight": "bolder", - "text": ($emoji + " mssql-python Issue Triage"), - "wrap": true, - "style": "heading" - }, - { - "type": "ColumnSet", - "columns": [ - { - "type": "Column", - "width": "auto", - "items": [ - { - "type": "TextBlock", - "text": $category_display, - "weight": "bolder", - "color": (if $category == "BREAK_FIX" then "attention" - elif $category == "BUG" then "warning" - else "default" end), - "size": "medium" - } - ] - }, - { - "type": "Column", - "width": "auto", - "items": [ - { - "type": "TextBlock", - "text": ("Severity: " + $severity), - "size": "medium", - "color": (if $severity == "critical" then "attention" - elif $severity == "high" then "warning" - else "default" end) - } - ] - }, - { - "type": "Column", - "width": "auto", - "items": [ - { - "type": "TextBlock", - "text": ("Confidence: " + $confidence + "%"), - "size": "medium" - } - ] - } - ] - }, - { - "type": "FactSet", - "separator": true, - "facts": [ - { "title": "Issue", "value": ("#" + $issue_num + " — " + $issue_title) }, - { "title": "Author", "value": ("@" + $issue_author) }, - { "title": "Keywords", "value": $keywords }, - { "title": "Relevant Files", "value": $relevant_files } - ] - }, - { - "type": "TextBlock", - "text": "**šŸ“ Analysis**", - "weight": "bolder", - "spacing": "medium", - "wrap": true - }, - { - "type": "TextBlock", - "text": $summary, - "wrap": true - }, - { - "type": "TextBlock", - "text": "**šŸ” Code Analysis**", - "weight": "bolder", - "spacing": "medium", - "wrap": true - }, - { - "type": "TextBlock", - "text": $code_analysis, - "wrap": true, - "fontType": "monospace", - "size": "small" - }, - { - "type": "TextBlock", - "text": ("⚔ **Action Required:** " + $action), - "weight": "bolder", - "color": "attention", - "spacing": "medium", - "wrap": true, - "separator": true - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "šŸ“‹ View Issue", - "url": $issue_url - }, - { - "type": "Action.OpenUrl", - "title": "šŸ“‚ View Repository", - "url": "https://github.com/microsoft/mssql-python" - } - ] - } - } - ] + "text": ( + "

" + $emoji + " mssql-python Issue Triage

" + + "

" + $category_display + "  |  " + + $sev_indicator + " Severity: " + $severity + "  |  " + + "Confidence: " + $confidence + "%

" + + "
" + + "

" + + "šŸ“Œ Issue: #" + $issue_num + " — " + $issue_title + "
" + + "šŸ‘¤ Author: @" + $issue_author + "
" + + "šŸ·ļø Keywords: " + $keywords + "
" + + "šŸ“‚ Relevant Files: " + $relevant_files + + "

" + + "
" + + "

šŸ“ Analysis

" + + "

" + $summary + "

" + + "

šŸ” Code Analysis

" + + "

" + $code_analysis + "

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

šŸ’” Engineer Guidance

" + + "

" + $engineer_guidance + "

" + else "" end) + + "
" + + "

⚔ Action Required: " + $action + "

" + + "

āš ļø AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.

" + + "

šŸ“‹ View Issue" + + "  |  " + + "šŸ“‚ View Repository

" + ) }' > /tmp/teams_payload.json echo "Sending notification to Teams Channel..." diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index f41327d3..f1718daa 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -14,6 +14,7 @@ on: permissions: issues: read contents: read + models: read jobs: triage: @@ -25,28 +26,22 @@ jobs: - name: Triage Analysis id: triage uses: actions/github-script@v7 - env: - AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} - AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} - AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - // --- Helper function for Azure OpenAI --- - async function callAzureOpenAI(prompt) { - const endpoint = process.env.AZURE_OPENAI_ENDPOINT; - const apiKey = process.env.AZURE_OPENAI_KEY; - const deployment = process.env.AZURE_OPENAI_DEPLOYMENT; - - const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=2025-01-01-preview`; + // --- Helper function for GitHub Models --- + async function callGitHubModels(prompt) { + const token = process.env.GITHUB_TOKEN; + const url = 'https://models.inference.ai.azure.com/chat/completions'; const response = await fetch(url, { method: "POST", headers: { - "api-key": apiKey, + "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ + model: "gpt-4.1", messages: [{ role: "user", content: prompt }], temperature: 0.1, response_format: { type: "json_object" } @@ -55,7 +50,7 @@ jobs: if (!response.ok) { const errText = await response.text(); - throw new Error(`Azure OpenAI error: ${response.status} - ${errText}`); + throw new Error(`GitHub Models error: ${response.status} - ${errText}`); } const data = await response.json(); @@ -87,7 +82,8 @@ jobs: // --- Classify the issue --- 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 Rust-based core (pybind) and Python wrappers. + 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 @@ -102,8 +98,8 @@ jobs: - mssql_python/logging.py — Logging infrastructure - mssql_python/row.py — Row objects - mssql_python/type.py — Type mappings - - mssql_python/ddbc_bindings.py — ODBC bindings - - mssql_python/pybind/ — Rust core bindings + - 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: @@ -131,7 +127,7 @@ jobs: let analysis; try { - const classifyResult = await callAzureOpenAI(classificationPrompt); + const classifyResult = await callGitHubModels(classificationPrompt); analysis = JSON.parse(classifyResult); } catch (e) { core.setFailed(`Classification failed: ${e.message}`); @@ -141,57 +137,107 @@ jobs: console.log(`Classification: ${analysis.category} (${analysis.confidence}%)`); console.log(`Severity: ${analysis.severity}`); + // --- Fetch relevant source files (for ALL categories) --- + console.log('Fetching relevant source files for code-grounded analysis...'); + const fileContents = []; + for (const filePath of analysis.relevant_source_files.slice(0, 3)) { + try { + const file = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: filePath + }); + const content = Buffer.from(file.data.content, 'base64').toString(); + 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)) { + if (['BUG', 'BREAK_FIX'].includes(analysis.category) && fileContents.length > 0) { console.log('Bug/Break-fix detected — analyzing codebase...'); - const fileContents = []; - for (const filePath of analysis.relevant_source_files.slice(0, 3)) { - try { - const file = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: filePath - }); - const content = Buffer.from(file.data.content, 'base64').toString(); - fileContents.push(`### File: ${filePath}\n\`\`\`python\n${content.slice(0, 3000)}\n\`\`\``); - } catch (e) { - console.log(`Could not fetch ${filePath}: ${e.message}`); - } + 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: ${issueTitle} + Body: ${issueBody.slice(0, 2000)} + ${codeContext} + + Provide analysis in JSON: + { + "is_bug": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", + "root_cause": "", + "affected_components": [""], + "evidence_and_context": "", + "recommended_fixes": ["", "", ""], + "code_locations": [""], + "risk_assessment": "" } + `; + + try { + codeAnalysis = await callGitHubModels(codePrompt); + console.log('Code analysis complete'); + } 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('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: ${issueTitle} + Issue Body: + ${issueBody.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": "", + "verdict": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", + "issue_identified": true/false, + "affected_files": [""], + "current_behavior": "", + "implementation_approach": "", + "effort_estimate": "small|medium|large|epic", + "risks_and_tradeoffs": "", + "suggested_response": "", + "related_considerations": [""] + } + + 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. + `; - if (fileContents.length > 0) { - 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 + Rust core). - - Bug Report: - Title: ${issueTitle} - Body: ${issueBody.slice(0, 2000)} - - Relevant source files: - ${fileContents.join('\n\n')} - - Provide analysis in JSON: - { - "is_bug": "confirmed|likely|uncertain|not_a_bug", - "root_cause": "", - "affected_components": [""], - "suggested_fix": "", - "risk_assessment": "", - "recommended_solutions": ["", "", ""] - } - `; - - try { - codeAnalysis = await callAzureOpenAI(codePrompt); - console.log('Code analysis complete'); - } catch (e) { - console.log(`Code analysis failed: ${e.message}`); - } + try { + engineerGuidance = await callGitHubModels(guidancePrompt); + console.log('Engineer guidance generated'); + } catch (e) { + console.log(`Engineer guidance failed: ${e.message}`); } } @@ -207,6 +253,7 @@ jobs: core.setOutput('relevant_files', analysis.relevant_source_files.join(', ')); core.setOutput('keywords', analysis.keywords.join(', ')); core.setOutput('code_analysis', codeAnalysis); + core.setOutput('engineer_guidance', engineerGuidance); core.setOutput('issue_number', issueNumber.toString()); core.setOutput('issue_title', issueTitle); core.setOutput('issue_url', issue.html_url); @@ -221,6 +268,7 @@ jobs: relevant_files: ${{ steps.triage.outputs.relevant_files }} keywords: ${{ steps.triage.outputs.keywords }} code_analysis: ${{ steps.triage.outputs.code_analysis }} + engineer_guidance: ${{ steps.triage.outputs.engineer_guidance }} issue_number: ${{ steps.triage.outputs.issue_number }} issue_title: ${{ steps.triage.outputs.issue_title }} issue_url: ${{ steps.triage.outputs.issue_url }} @@ -238,6 +286,7 @@ jobs: relevant_files: ${{ needs.triage.outputs.relevant_files }} keywords: ${{ needs.triage.outputs.keywords }} code_analysis: ${{ needs.triage.outputs.code_analysis }} + engineer_guidance: ${{ needs.triage.outputs.engineer_guidance }} 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 3f9bd64e..5f7378b6 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,7 @@ mssql_py_core/ # learning files learnings/ + +# Triage test output files +triage-out*.txt +triage-err*.txt diff --git a/test-triage-local.js b/test-triage-local.js new file mode 100644 index 00000000..d89788d5 --- /dev/null +++ b/test-triage-local.js @@ -0,0 +1,435 @@ +/** + * 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 + */ + +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 "); + process.exit(1); +} + +// --- Helper: GitHub Models --- +async function callGitHubModels(prompt) { + const token = process.env.GH_TOKEN; + const url = "https://models.inference.ai.azure.com/chat/completions"; + + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "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" ? "🟔" : "🟢"; + + 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, ">"); + const parts = []; + if (parsed.is_bug) parts.push(`Verdict: ${escVal(parsed.is_bug)}`); + if (parsed.root_cause) parts.push(`Root Cause: ${escVal(parsed.root_cause)}`); + if (parsed.affected_components && parsed.affected_components.length > 0) { + parts.push(`Affected Components:
${parsed.affected_components.map(c => `  ā€¢ ${escVal(c)}`).join("
")}`); + } + if (parsed.evidence_and_context) parts.push(`Evidence & Context: ${escVal(parsed.evidence_and_context)}`); + if (parsed.recommended_fixes && parsed.recommended_fixes.length > 0) { + parts.push(`Recommended Fixes:
${parsed.recommended_fixes.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("
")}`); + } + if (parsed.code_locations && parsed.code_locations.length > 0) { + parts.push(`Code Locations:
${parsed.code_locations.map(l => `  ā€¢ ${escVal(l)}`).join("
")}`); + } + if (parsed.risk_assessment) parts.push(`Risk Assessment: ${escVal(parsed.risk_assessment)}`); + codeAnalysisText = parts.join("

"); + } catch (e) { + // If JSON parsing fails, show raw text (truncated) + codeAnalysisText = 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, ">"); + const parts = []; + if (parsed.technical_assessment) parts.push(`Technical Assessment: ${escVal(parsed.technical_assessment)}`); + if (parsed.verdict) parts.push(`Verdict: ${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(`Affected Files:
${parsed.affected_files.map(a => `  ā€¢ ${escVal(a)}`).join("
")}`); + } + if (parsed.implementation_approach) parts.push(`Implementation Approach: ${escVal(parsed.implementation_approach)}`); + if (parsed.risks_and_tradeoffs) parts.push(`Risks & Tradeoffs: ${escVal(parsed.risks_and_tradeoffs)}`); + if (parsed.suggested_response) parts.push(`Suggested Response to User:
${escVal(parsed.suggested_response)}`); + if (parsed.related_considerations && parsed.related_considerations.length > 0) { + parts.push(`Related Considerations:
${parsed.related_considerations.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("
")}`); + } + engineerGuidanceText = parts.join("

"); + } catch (e) { + engineerGuidanceText = esc(engineerGuidance); + if (engineerGuidanceText.length > 3000) { + engineerGuidanceText = engineerGuidanceText.slice(0, 3000) + "... (truncated)"; + } + } + } + + const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); + + const htmlMessage = [ + `

${emoji} mssql-python Issue Triage

`, + `

${esc(categoryDisplay)}  |  `, + `${sevIndicator} Severity: ${esc(severity)}  |  `, + `Confidence: ${analysis.confidence}%

`, + `
`, + `

`, + `šŸ“Œ Issue: #${issue.number} — ${esc(issue.title)}
`, + `šŸ‘¤ Author: @${esc(issue.user.login)}
`, + `šŸ·ļø Keywords: ${esc(analysis.keywords.join(", "))}
`, + `šŸ“‚ Relevant Files: ${esc(analysis.relevant_source_files.join(", "))}`, + `

`, + `
`, + `

šŸ“ Analysis

`, + `

${esc(analysis.summary_for_maintainers)}

`, + `

šŸ” Code Analysis

`, + `

${codeAnalysisText}

`, + engineerGuidanceText ? `

šŸ’” Engineer Guidance

` : '', + engineerGuidanceText ? `

${engineerGuidanceText}

` : '', + `
`, + `

⚔ Action Required: ${esc(action)}

`, + `

āš ļø AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.

`, + `

šŸ“‹ View Issue`, + `  |  `, + `šŸ“‚ View Repository

`, + ].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": [""], + "keywords": [""], + "summary_for_maintainers": "" +} + +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": "", + "affected_components": [""], + "evidence_and_context": "", + "recommended_fixes": ["", "", ""], + "code_locations": [""], + "risk_assessment": "" +} +`; + + 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": "", + "verdict": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", + "issue_identified": true/false, + "affected_files": [""], + "current_behavior": "", + "implementation_approach": "", + "effort_estimate": "small|medium|large|epic", + "risks_and_tradeoffs": "", + "suggested_response": "", + "related_considerations": [""] +} + +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); +}); From 72557ac54b08b09ddc598930b1aa38d4678a6f51 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 18 Mar 2026 01:33:11 +0530 Subject: [PATCH 3/6] CHORE: Add permissions block to issue-notify.yml for CodeQL compliance --- .github/workflows/issue-notify.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index d89e8507..96187ebd 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -48,6 +48,9 @@ on: TEAMS_WEBHOOK_URL: required: true +permissions: + contents: read + jobs: send-notification: runs-on: ubuntu-latest From bbd0c01c88d5b215bd1ab7d0c7ce5d4e18c3db5b Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 18 Mar 2026 01:37:00 +0530 Subject: [PATCH 4/6] FIX: Move esc() helper before usage to prevent ReferenceError in fallback paths --- test-triage-local.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test-triage-local.js b/test-triage-local.js index d89788d5..d5d999d8 100644 --- a/test-triage-local.js +++ b/test-triage-local.js @@ -125,6 +125,7 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i : severity === "high" ? "🟠" : severity === "medium" ? "🟔" : "🟢"; + const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); let codeAnalysisText = "N/A — classification did not require code analysis."; let engineerGuidanceText = ""; @@ -150,8 +151,7 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i if (parsed.risk_assessment) parts.push(`Risk Assessment: ${escVal(parsed.risk_assessment)}`); codeAnalysisText = parts.join("

"); } catch (e) { - // If JSON parsing fails, show raw text (truncated) - codeAnalysisText = codeAnalysis; + codeAnalysisText = esc(codeAnalysis); if (codeAnalysisText.length > 3000) { codeAnalysisText = codeAnalysisText.slice(0, 3000) + "... (truncated)"; } @@ -184,8 +184,6 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i } } - const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); - const htmlMessage = [ `

${emoji} mssql-python Issue Triage

`, `

${esc(categoryDisplay)}  |  `, From d8757f505cefe797aa048f9f26adeafc6448e458 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 18 Mar 2026 01:45:54 +0530 Subject: [PATCH 5/6] FIX: HTML-escape untrusted values, wire justification into notification, use
for code analysis formatting --- .github/workflows/issue-notify.yml | 50 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 96187ebd..29f5b186 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -69,7 +69,7 @@ jobs: INPUT_SUMMARY: ${{ inputs.summary_for_maintainers }} INPUT_CODE_ANALYSIS: ${{ inputs.code_analysis }} INPUT_ENGINEER_GUIDANCE: ${{ inputs.engineer_guidance }} - INPUT_ACTION_TEXT: ${{ inputs.justification }} + INPUT_JUSTIFICATION: ${{ inputs.justification }} TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} run: | CATEGORY="$INPUT_CATEGORY" @@ -110,21 +110,21 @@ jobs: # Try to parse as JSON and extract structured fields CODE_ANALYSIS=$(echo "$CODE_ANALYSIS_RAW" | jq -r ' [ - (if .is_bug then "Verdict: " + .is_bug else empty end), - (if .root_cause then "\nRoot Cause: " + .root_cause else empty end), + (if .is_bug then "Verdict: " + (.is_bug | @html) else empty end), + (if .root_cause then "Root Cause: " + (.root_cause | @html) else empty end), (if .affected_components and (.affected_components | length) > 0 - then "\nAffected Components:\n" + ([.affected_components[] | " • " + .] | join("\n")) + then "Affected Components:
" + ([.affected_components[] | "  ā€¢ " + (. | @html)] | join("
")) else empty end), - (if .evidence_and_context then "\nEvidence & Context: " + .evidence_and_context else empty end), + (if .evidence_and_context then "Evidence & Context: " + (.evidence_and_context | @html) else empty end), (if .recommended_fixes and (.recommended_fixes | length) > 0 - then "\nRecommended Fixes:\n" + ([.recommended_fixes | to_entries[] | " " + ((.key + 1) | tostring) + ". " + .value] | join("\n")) + then "Recommended Fixes:
" + ([.recommended_fixes | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value | @html)] | join("
")) else empty end), (if .code_locations and (.code_locations | length) > 0 - then "\nCode Locations:\n" + ([.code_locations[] | " • " + .] | join("\n")) + then "Code Locations:
" + ([.code_locations[] | "  ā€¢ " + (. | @html)] | join("
")) else empty end), - (if .risk_assessment then "\nRisk Assessment: " + .risk_assessment else empty end) - ] | join("\n") - ' 2>/dev/null || echo "$CODE_ANALYSIS_RAW") + (if .risk_assessment then "Risk Assessment: " + (.risk_assessment | @html) else empty end) + ] | join("

") + ' 2>/dev/null || echo "$CODE_ANALYSIS_RAW" | sed 's/&/\&/g; s//\>/g') else CODE_ANALYSIS="N/A — classification did not require code analysis." fi @@ -134,20 +134,20 @@ jobs: if [ -n "$ENGINEER_GUIDANCE_RAW" ]; then ENGINEER_GUIDANCE=$(echo "$ENGINEER_GUIDANCE_RAW" | jq -r ' [ - (if .technical_assessment then "Technical Assessment: " + .technical_assessment else empty end), - (if .verdict then "Verdict: " + .verdict else empty end), - (if .effort_estimate then "Effort Estimate: " + .effort_estimate else empty end), + (if .technical_assessment then "Technical Assessment: " + (.technical_assessment | @html) else empty end), + (if .verdict then "Verdict: " + (.verdict | @html) else empty end), + (if .effort_estimate then "Effort Estimate: " + (.effort_estimate | @html) else empty end), (if .affected_files and (.affected_files | length) > 0 - then "Affected Files:
" + ([.affected_files[] | "  ā€¢ " + .] | join("
")) + then "Affected Files:
" + ([.affected_files[] | "  ā€¢ " + (. | @html)] | join("
")) else empty end), - (if .implementation_approach then "Implementation Approach: " + .implementation_approach else empty end), - (if .risks_and_tradeoffs then "Risks & Tradeoffs: " + .risks_and_tradeoffs else empty end), - (if .suggested_response then "Suggested Response to User:
" + .suggested_response 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] | join("
")) + then "Related Considerations:
" + ([.related_considerations | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value | @html)] | join("
")) else empty end) ] | join("

") - ' 2>/dev/null || echo "$ENGINEER_GUIDANCE_RAW") + ' 2>/dev/null || echo "$ENGINEER_GUIDANCE_RAW" | sed 's/&/\&/g; s//\>/g') else ENGINEER_GUIDANCE="" fi @@ -176,6 +176,7 @@ jobs: --arg summary "$INPUT_SUMMARY" \ --arg code_analysis "$CODE_ANALYSIS" \ --arg engineer_guidance "$ENGINEER_GUIDANCE" \ + --arg justification "$INPUT_JUSTIFICATION" \ --arg action "$ACTION" \ --arg repo_url "https://github.com/microsoft/mssql-python" \ '{ @@ -186,14 +187,15 @@ jobs: "Confidence: " + $confidence + "%

" + "
" + "

" + - "šŸ“Œ Issue: #" + $issue_num + " — " + $issue_title + "
" + - "šŸ‘¤ Author: @" + $issue_author + "
" + - "šŸ·ļø Keywords: " + $keywords + "
" + - "šŸ“‚ Relevant Files: " + $relevant_files + + "šŸ“Œ Issue: #" + $issue_num + " — " + ($issue_title | @html) + "
" + + "šŸ‘¤ Author: @" + ($issue_author | @html) + "
" + + "šŸ·ļø Keywords: " + ($keywords | @html) + "
" + + "šŸ“‚ Relevant Files: " + ($relevant_files | @html) + "
" + + "šŸ’¬ Justification: " + ($justification | @html) + "

" + "
" + "

šŸ“ Analysis

" + - "

" + $summary + "

" + + "

" + ($summary | @html) + "

" + "

šŸ” Code Analysis

" + "

" + $code_analysis + "

" + (if $engineer_guidance != "" then From 20a29637307bd8b542ecab5b31f255ef0aebdf68 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 18 Mar 2026 11:28:45 +0530 Subject: [PATCH 6/6] CHORE: Increase triage wait time to 60 minutes --- .github/workflows/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index f1718daa..69af873b 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Wait for acknowledgement - run: sleep 120 + run: sleep 3600 - name: Triage Analysis id: triage