Conversation
…und suggestions to AI triage - Added dedicated LLM call to generate copy-paste-ready customer responses for all issue categories - Added similar/duplicate issue detection: searches recent open + closed issues via GitHub API, uses LLM to identify duplicates and recently fixed related issues - Added workaround generation for BUG/BREAK_FIX: suggests practical workarounds with code snippets and downgrade guidance - Added 'Similar Issues & Recent Fixes', 'Workarounds', and 'Suggested Response to Customer' sections in Teams notification card - Updated test-triage-local.js to match new workflow logic - Similar issues and workaround context fed into customer response for richer replies
There was a problem hiding this comment.
Pull request overview
Enhances the repository’s GitHub issue triage automation (and its local test harness) by adding three new AI-assisted outputs—similar-issue detection, workaround suggestions, and a suggested customer response—so engineers can respond faster and with more context in the Teams notification.
Changes:
- Add a “Similar Issues & Recent Fixes” analysis step and surface it through workflow outputs.
- Add a “Workarounds” generation step for BUG/BREAK_FIX issues and surface it through workflow outputs.
- Add a dedicated “Suggested Response to Customer” generation step and include it in Teams notifications + local triage test script.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
test-triage-local.js |
Extends local triage flow to fetch/format similar issues, generate workarounds, and generate a suggested response in the Teams payload. |
.github/workflows/issue-triage.yml |
Adds additional GitHub Models calls for similar issues, workarounds, and suggested response; publishes them as job outputs and passes them to notification workflow. |
.github/workflows/issue-notify.yml |
Adds new reusable-workflow inputs and renders new sections in the Teams HTML card. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [ | ||
| (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), | ||
| (if .duplicate_issues and (.duplicate_issues | length) > 0 | ||
| then "<b>Similar/Duplicate Issues:</b><br>" + ([.duplicate_issues[] | " • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " [" + .state + "] — <i>" + (.similarity | @html) + ":</i> " + (.explanation | @html)] | join("<br>")) | ||
| else empty end), | ||
| (if .recently_fixed and (.recently_fixed | length) > 0 | ||
| then "<b>Recently Fixed:</b><br>" + ([.recently_fixed[] | " • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("<br>")) |
There was a problem hiding this comment.
The Teams HTML for similar issues interpolates LLM-provided fields into HTML/attributes without consistently escaping them (e.g., .state is inserted raw into the string, and .issue_number is used inside an href). Since this JSON comes from the model output, treat it as untrusted: HTML-escape all displayed fields (including state) and URI/number-validate issue_number before using it in a link.
| [ | |
| (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), | |
| (if .duplicate_issues and (.duplicate_issues | length) > 0 | |
| then "<b>Similar/Duplicate Issues:</b><br>" + ([.duplicate_issues[] | " • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " [" + .state + "] — <i>" + (.similarity | @html) + ":</i> " + (.explanation | @html)] | join("<br>")) | |
| else empty end), | |
| (if .recently_fixed and (.recently_fixed | length) > 0 | |
| then "<b>Recently Fixed:</b><br>" + ([.recently_fixed[] | " • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("<br>")) | |
| def valid_issue_number: | |
| (.issue_number | tostring) as $n | |
| | if ($n | test("^[0-9]+$")) then $n else null end; | |
| def issue_link: | |
| valid_issue_number as $n | |
| | if $n != null | |
| then "<a href=\"https://github.com/microsoft/mssql-python/issues/" + $n + "\">#" + ($n | @html) + "</a>" | |
| else "#" + ((.issue_number | tostring) | @html) | |
| end; | |
| [ | |
| (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), | |
| (if .duplicate_issues and (.duplicate_issues | length) > 0 | |
| then "<b>Similar/Duplicate Issues:</b><br>" + ([.duplicate_issues[] | " • " + issue_link + " " + (.title | @html) + " [" + (.state | @html) + "] — <i>" + (.similarity | @html) + ":</i> " + (.explanation | @html)] | join("<br>")) | |
| else empty end), | |
| (if .recently_fixed and (.recently_fixed | length) > 0 | |
| then "<b>Recently Fixed:</b><br>" + ([.recently_fixed[] | " • " + issue_link + " " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("<br>")) |
| const openRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open&per_page=30&sort=created&direction=desc`, { headers }); | ||
| const openIssues = await openRes.json(); | ||
|
|
||
| const closedRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=closed&per_page=30&sort=updated&direction=desc`, { headers }); | ||
| const closedIssues = await closedRes.json(); | ||
|
|
||
| const allIssues = [...openIssues, ...closedIssues] | ||
| .filter(i => i.number !== issueNumber && !i.pull_request) | ||
| .map(i => ({ |
There was a problem hiding this comment.
The similar-issues fetch path assumes the GitHub API responses are arrays. If openRes/closedRes is non-2xx (rate limit / auth / abuse detection), await res.json() will be an error object, and [...openIssues, ...closedIssues] will throw. Add response.ok checks (and/or validate Array.isArray(...)) before spreading so the script fails gracefully with a clear message.
| const parts = []; | ||
| if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); | ||
| if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { | ||
| parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.map(d => | ||
| ` • <a href="https://github.com/microsoft/mssql-python/issues/${d.issue_number}">#${d.issue_number}</a> ${escVal(d.title)} [${d.state}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` | ||
| ).join("<br>")}`); | ||
| } | ||
| if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { | ||
| parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.map(f => | ||
| ` • <a href="https://github.com/microsoft/mssql-python/issues/${f.issue_number}">#${f.issue_number}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` | ||
| ).join("<br>")}`); |
There was a problem hiding this comment.
issue_number from the LLM output is interpolated directly into an HTML attribute (href=.../${d.issue_number}) without validation/escaping. Because the JSON originates from model output (which can be influenced by issue content), this allows HTML injection in the Teams payload. Coerce issue_number to a number (or strictly whitelist ^[0-9]+$) and escape/encode it before building the URL.
| const parts = []; | |
| if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); | |
| if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { | |
| parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.map(d => | |
| ` • <a href="https://github.com/microsoft/mssql-python/issues/${d.issue_number}">#${d.issue_number}</a> ${escVal(d.title)} [${d.state}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` | |
| ).join("<br>")}`); | |
| } | |
| if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { | |
| parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.map(f => | |
| ` • <a href="https://github.com/microsoft/mssql-python/issues/${f.issue_number}">#${f.issue_number}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` | |
| ).join("<br>")}`); | |
| const safeIssueNumber = (value) => { | |
| const normalized = String(value).trim(); | |
| return /^[0-9]+$/.test(normalized) ? normalized : null; | |
| }; | |
| const parts = []; | |
| if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); | |
| if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { | |
| parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.map(d => { | |
| const issueNumber = safeIssueNumber(d.issue_number); | |
| const issueLink = issueNumber | |
| ? `<a href="https://github.com/microsoft/mssql-python/issues/${encodeURIComponent(issueNumber)}">#${issueNumber}</a>` | |
| : `#${escVal(d.issue_number)}`; | |
| return ` • ${issueLink} ${escVal(d.title)} [${escVal(d.state)}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}`; | |
| }).join("<br>")}`); | |
| } | |
| if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { | |
| parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.map(f => { | |
| const issueNumber = safeIssueNumber(f.issue_number); | |
| const issueLink = issueNumber | |
| ? `<a href="https://github.com/microsoft/mssql-python/issues/${encodeURIComponent(issueNumber)}">#${issueNumber}</a>` | |
| : `#${escVal(f.issue_number)}`; | |
| return ` • ${issueLink} ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}`; | |
| }).join("<br>")}`); |
| suggestedResponse ? `<hr>` : '', | ||
| suggestedResponse ? `<h3>✉️ Suggested Response to Customer</h3>` : '', | ||
| suggestedResponse ? `<p><i>Copy-paste or edit the response below and post it on the issue:</i></p>` : '', | ||
| suggestedResponse ? `<blockquote>${esc(suggestedResponse)}</blockquote>` : '', |
There was a problem hiding this comment.
The workflow version converts the suggested response into HTML with <br> line breaks, but the local test script renders it inside <blockquote> without converting newlines. This makes the local preview diverge from the workflow output and can make the response hard to read. Consider replacing \n with <br> (after escaping) to match the workflow behavior.
| suggestedResponse ? `<blockquote>${esc(suggestedResponse)}</blockquote>` : '', | |
| suggestedResponse ? `<blockquote>${esc(suggestedResponse).replace(/\n/g, "<br>")}</blockquote>` : '', |
… hrefs - Escape .state and .confidence via @html (jq) / escVal() (JS) instead of raw interpolation - Validate .issue_number as numeric-only before using in href attributes (jq: test, JS: regex filter + Number()) - Entries with non-numeric issue_number are silently dropped from the output - Applied consistently in issue-notify.yml and test-triage-local.js
- Replace @html/@escval escaping of confidence with strict allowlist validation - Only 'high', 'medium', 'low' are accepted; anything else renders as 'unknown' - Prevents HTML injection via unexpected confidence values from LLM output - Applied in both issue-notify.yml (jq) and test-triage-local.js
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. 📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 67.8%
mssql_python.row.py: 70.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 74.4%
mssql_python.pybind.connection.connection.cpp: 75.8%
mssql_python.__init__.py: 77.3%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 79.6%
mssql_python.connection.py: 85.2%🔗 Quick Links
|
Work Item / Issue Reference
AB#https://sqlclientdrivers.visualstudio.com/mssql-python/_workitems/edit/44588
GitHub Issue: #<ISSUE_NUMBER>
Summary
This pull request significantly enhances the GitHub issue triage and notification workflows by introducing automated detection and reporting of similar issues, suggesting practical workarounds, and generating draft customer responses. The changes streamline information flow between the triage and notification steps, improving both the quality and clarity of information provided to maintainers and engineers.
Key improvements include:
Automated Issue Analysis and Enrichment
Workflow Output and Notification Enhancements
Refactoring and Prompt Improvements
These enhancements provide maintainers and engineers with richer, more actionable information for each issue, improving triage efficiency and the quality of user communication.
References:
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]