Skip to content

Add e2e concurrent/stress test for AcquireTokenSilent via broker, Fixes AB#3582859 #206

Add e2e concurrent/stress test for AcquireTokenSilent via broker, Fixes AB#3582859

Add e2e concurrent/stress test for AcquireTokenSilent via broker, Fixes AB#3582859 #206

# GitHub Copilot Issue Response Workflow
# Automatically triages and responds to issues using AI.
name: Copilot Issue Triage
on:
issues:
types: [opened, reopened, labeled]
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
concurrency:
group: issue-triage-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
triage:
name: Triage New Issue
runs-on: ubuntu-latest
if: >-
github.event_name == 'issues' &&
(github.event.action == 'opened' ||
(github.event.action == 'labeled' && github.event.label.name == 'Target-For-Ai-Triage'))
outputs:
issue_type: ${{ steps.ai_triage.outputs.issue_type }}
issue_type_reason: ${{ steps.ai_triage.outputs.issue_type_reason }}
msal_version: ${{ steps.ai_triage.outputs.msal_version }}
device_count: ${{ steps.ai_triage.outputs.device_count }}
ai_solution: ${{ steps.ai_triage.outputs.ai_solution }}
needs_info: ${{ steps.ai_triage.outputs.needs_info }}
needs_info_reason: ${{ steps.ai_triage.outputs.needs_info_reason }}
related_issues: ${{ steps.ai_triage.outputs.related_issues }}
very_old_msal: ${{ steps.ai_triage.outputs.very_old_msal }}
version_age_days: ${{ steps.ai_triage.outputs.version_age_days }}
version_published_date: ${{ steps.ai_triage.outputs.version_published_date }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: AI-Powered Triage
id: ai_triage
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const fs = require('fs');
const issue = context.payload.issue;
function redactSecrets(text) {
if (!text) return '';
return text
.replace(/eyJ[A-Za-z0-9_\-+\/]{10,}\.[A-Za-z0-9_\-+\/]+\.[A-Za-z0-9_\-+\/]*/g, '[REDACTED_JWT]')
.replace(/Bearer\s+[A-Za-z0-9_\-./+=]+/gi, 'Bearer [REDACTED]')
.replace(/(client_secret|access_token|refresh_token|api[_-]?key)["\s:=]+[^\s"',}]+/gi, '$1=[REDACTED]')
.replace(/[A-Za-z0-9+\/]{100,}={0,2}/g, '[REDACTED_BASE64]');
}
const redactedTitle = redactSecrets(issue.title);
const redactedBody = redactSecrets(issue.body || '');
let commonIssuesGuide = '';
try {
commonIssuesGuide = fs.readFileSync('.github/issue-responses/common-issues-guide.md', 'utf8');
} catch(e) {
console.error('Failed to load common issues guide:', e.message);
}
let releases = [];
let latestVersion = '8.+';
try {
const latestRelease = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
latestVersion = latestRelease.data.tag_name.replace(/^v/, '');
// Also fetch all releases for version age checking
releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner, repo: context.repo.repo, per_page: 100
});
} catch(e) {
console.error('Failed to fetch releases:', e.message);
}
const systemPrompt = [
'You are an expert MSAL Android support engineer. Analyze GitHub issues and provide classification and solutions.',
'',
'=== COMMON ISSUES REFERENCE ===',
'The following guide contains solutions for 250+ issues organized by category.',
'Use the "Error Pattern Quick Lookup" section to quickly find relevant solutions.',
'Always cite related GitHub issues (format: #NNNN) when providing solutions.',
'',
commonIssuesGuide,
'',
'=== TRIAGE INSTRUCTIONS ===',
'ISSUE TYPE CLASSIFICATION:',
'- Bug: Library bug, unexpected behavior when MSAL used correctly',
'- User-Error: Incorrect MSAL Implementation causing unexpected behavior',
'- Feature-Request: Desired behavior not currently supported',
'- Question: User asking for clarification, no error/unexpected behavior',
'- Other: Miscellaneous, no clear classification',
'',
'=== SOLUTION REQUIREMENTS ===',
'In suggestedSolution field:',
'1. Identify the most similar issue from the Common Issues Reference',
'2. Provide specific solution with code examples if applicable',
'3. List related GitHub issues from the guide (e.g., "See also: #1234, #5678")',
'4. If issue not in guide, provide general MSAL troubleshooting steps',
'',
'Respond with ONLY valid JSON:',
'{',
' "issueType": "Bug|User-Error|Feature-Request|Question|Other",',
' "issueTypeReason": "string explaining reasoning for choosing a particular issue type",',
' "deviceCount": "number or null if not found in issue title or body",',
' "msalVersion": "extracted MSAL version string from the issue title/body or null if not found",',
' "needsMoreInfo": "boolean, true if issue has insufficient information for triage",',
' "needsMoreInfoReason": "if needsMoreInfo = true, provide string explaining reasoning for the tag, what information is missing?",',
' "suggestedSolution": "detailed solution referencing Common Issues guide sections and related GitHub issues",',
' "relatedIssues": "array of related GitHub issue numbers from the guide, e.g. [1234, 5678]",',
'}'
].join('\n');
const userPrompt = [
'Issue Title: ' + redactedTitle,
'',
'Issue Description:',
redactedBody,
'',
'Available Releases (for version age check):',
releases.slice(0, 60).map(r => r.tag_name + ': ' + r.published_at).join('\n'),
'',
'Latest MSAL Version: ' + latestVersion,
'',
'Analyze this issue and provide comprehensive triage in JSON format according to your system prompt. Make sure to use the MSAL AI guidance files.'
].join('\n');
let result = {
issueType: 'Other',
issueTypeReason: '',
deviceCount: null,
msalVersion: null,
needsMoreInfo: true,
needsMoreInfoReason: '',
suggestedSolution: null,
relatedIssues: []
};
try {
console.log('Calling GitHub Models API for triage...');
const response = await fetch('https://models.github.ai/inference/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
]
})
});
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + await response.text());
const data = await response.json();
const content = data.choices[0].message.content;
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
result = { ...result, ...JSON.parse(jsonMatch[0]) };
console.log('AI triage result:', JSON.stringify(result));
} else {
console.log("There was an error with the json match");
}
} catch (error) {
console.error('AI triage error:', error.message);
}
core.setOutput('issue_type', result.issueType);
core.setOutput('issue_type_reason', result.issueTypeReason);
core.setOutput('msal_version', result.msalVersion || '');
core.setOutput('device_count', result.deviceCount);
core.setOutput('needs_info', result.needsMoreInfo.toString());
core.setOutput('needs_info_reason', result.needsMoreInfoReason || '');
core.setOutput('ai_solution', result.suggestedSolution || '');
core.setOutput('related_issues', JSON.stringify(result.relatedIssues || []));
// Calculate version age for version update recommendations
let versionAgeDays = null;
let versionPublishedDate = null;
let isVeryOldMsal = false;
if (result.msalVersion && result.msalVersion !== 'null' && releases.length > 0) {
const normalizedVersion = result.msalVersion.replace(/^v/, '');
const matchingRelease = releases.find(release => {
const releaseVersion = release.tag_name.replace(/^v/, '');
return releaseVersion === normalizedVersion || release.tag_name === result.msalVersion;
});
if (matchingRelease) {
const publishedDate = new Date(matchingRelease.published_at);
const currentDate = new Date();
versionAgeDays = Math.floor((currentDate - publishedDate) / (1000 * 60 * 60 * 24));
versionPublishedDate = publishedDate.toISOString().split('T')[0];
isVeryOldMsal = versionAgeDays > 548; // 1.5 years
console.log(`MSAL version ${result.msalVersion}: ${versionAgeDays} days old (published ${versionPublishedDate}), very old: ${isVeryOldMsal}`);
}
}
core.setOutput('very_old_msal', isVeryOldMsal.toString());
core.setOutput('version_age_days', versionAgeDays ? versionAgeDays.toString() : '');
core.setOutput('version_published_date', versionPublishedDate || '');
- name: Apply Labels
id: apply_labels
uses: actions/github-script@v7
env:
ISSUE_TYPE: ${{ steps.ai_triage.outputs.issue_type }}
DEVICE_COUNT: ${{ steps.ai_triage.outputs.device_count }}
NEEDS_INFO: ${{ steps.ai_triage.outputs.needs_info }}
MSAL_VERSION: ${{ steps.ai_triage.outputs.msal_version }}
VERY_OLD_MSAL: ${{ steps.ai_triage.outputs.very_old_msal }}
with:
script: |
const labels = [process.env.ISSUE_TYPE];
// Check if MSAL version is older than 1.5 years (548 days)
if (process.env.VERY_OLD_MSAL === 'true') {
labels.push('very-old-msal');
console.log(`MSAL version ${process.env.MSAL_VERSION} is very old, adding label`);
}
if (process.env.NEEDS_INFO === 'true') labels.push('needs-more-info');
const labelColors = {
'Bug': 'd93f0b',
'User-Error': 'b0e9e0',
'Feature-Request': 'd7f79b',
'Question': '1d76db',
'Other': '93a588',
'very-old-msal': 'ffa500',
'p1-High': 'd93f0b',
'p2-Medium': 'fbca04',
'p3-Low': 'fef2c0',
'p4-Minor': 'c5def5'
};
for (const label of labels) {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label
});
} catch {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
color: labelColors[label] || '666666'
});
} catch(e) {
console.log('Could not create label ' + label);
}
}
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labels
});
// Remove the trigger label if it was used
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'Target-For-Ai-Triage'
});
} catch(e) {
// Label wasn't present, ignore
}
respond:
name: Generate Response
needs: triage
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate and Post Response
uses: actions/github-script@v7
env:
ISSUE_TYPE: ${{ needs.triage.outputs.issue_type }}
ISSUE_TYPE_REASON: ${{ needs.triage.outputs.issue_type_reason }}
MSAL_VERSION: ${{ needs.triage.outputs.msal_version }}
NEEDS_INFO: ${{ needs.triage.outputs.needs_info }}
NEEDS_INFO_REASON: ${{ needs.triage.outputs.needs_info_reason }}
AI_SOLUTION: ${{ needs.triage.outputs.ai_solution }}
RELATED_ISSUES: ${{ needs.triage.outputs.related_issues }}
VERY_OLD_MSAL: ${{ needs.triage.outputs.very_old_msal }}
VERSION_AGE_DAYS: ${{ needs.triage.outputs.version_age_days }}
VERSION_PUBLISHED_DATE: ${{ needs.triage.outputs.version_published_date }}
with:
script: |
const issueType = process.env.ISSUE_TYPE;
const issueTypeReason = process.env.ISSUE_TYPE_REASON;
const msalVersion = process.env.MSAL_VERSION;
const needsInfo = process.env.NEEDS_INFO === 'true';
const needInfoReason = process.env.NEEDS_INFO_REASON;
const aiSolution = process.env.AI_SOLUTION;
const relatedIssues = JSON.parse(process.env.RELATED_ISSUES || '[]');
const isVeryOld = process.env.VERY_OLD_MSAL === 'true';
const versionAgeDays = process.env.VERSION_AGE_DAYS;
const versionPublishedDate = process.env.VERSION_PUBLISHED_DATE;
let latestVersion = '8.+';
try {
const rel = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
latestVersion = rel.data.tag_name.replace(/^v/, '');
} catch(e) {}
let response = 'Thank you for opening this issue!\n\n';
if (isVeryOld) {
response += '⚠️ **Unsupported MSAL Version Detected**\n\n';
response += 'Version **' + msalVersion + '** (released ' + versionPublishedDate + ', ' + versionAgeDays + ' days ago) is more than 1.5 years old and is no longer supported.\n\n';
response += '**Please upgrade to the latest version (' + latestVersion + '):**\n';
response += '```gradle\nimplementation "com.microsoft.identity.client:msal:8.+"\n```\n\n';
response += 'If the issue persists after upgrading, please reopen with updated details.\n\n';
response += '---\n*Automated response. A team member will review if needed.*';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: response
});
return;
}
response += '**Labels Applied:**\n';
response += '`' + issueType + '`: ' + issueTypeReason + '.\n';
if (needsInfo) response += '`needs-more-info`: ' + needInfoReason + '.\n';
response += '\n';
if (aiSolution) {
response += '## 🤖 AI Analysis\n\n' + aiSolution + '\n\n';
if (relatedIssues && relatedIssues.length > 0) {
response += '**Related Issues:** ';
response += relatedIssues.map(num => '#' + num).join(', ');
response += '\n\n';
}
response += '---\n\n';
}
// Version update recommendation - only show if user is not on latest version
if (msalVersion && msalVersion !== 'null' && msalVersion !== latestVersion) {
response += '## 📦 MSAL Version Update Available\n\n';
response += 'You are using MSAL version **' + msalVersion + '**. ';
response += 'The latest version is **' + latestVersion + '**.\n\n';
response += 'We recommend updating to the latest version to benefit from bug fixes, security improvements, and new features.\n\n';
response += '```gradle\nimplementation "com.microsoft.identity.client:msal:8.+"\n```\n\n';
response += '> Using `8.+` automatically gets patch updates within the 8.x series.\n\n';
response += '---\n\n';
}
if (needsInfo) {
response += '**To help us assist you, please provide:**\n';
if (!msalVersion || msalVersion === 'null') {
response += '- MSAL version (latest: ' + latestVersion + ')\n';
}
response += '- Android version and device\n';
response += '- Complete error message/stack trace\n';
response += '- Steps to reproduce\n\n';
}
response += '---\n\n**Need help?** Comment with:\n```\nPING-COPILOT: <your question>\n```\n\n';
response += '> **Privacy:** Do not include credentials, tokens, or PII in PING-COPILOT requests.\n\n';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: response
});
handle_ping_copilot:
name: Handle PING-COPILOT
runs-on: ubuntu-latest
if: >-
github.event_name == 'issue_comment' &&
github.event.action == 'created' &&
(contains(github.event.comment.body, 'PING-COPILOT') ||
contains(github.event.comment.body, 'PING COPILOT') ||
contains(github.event.comment.body, 'ping-copilot') ||
contains(github.event.comment.body, 'ping copilot'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Process PING-COPILOT Request
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const fs = require('fs');
const comment = context.payload.comment.body;
const issue = context.payload.issue;
// Match PING-COPILOT or PING COPILOT (with or without colon, case-insensitive)
const match = comment.match(/PING[-\s]COPILOT:?\s*([\s\S]+)/i);
if (!match) return;
const userRequest = match[1].trim();
function redactSecrets(text) {
if (!text) return '';
return text
.replace(/eyJ[A-Za-z0-9_\-+\/]{10,}\.[A-Za-z0-9_\-+\/]+\.[A-Za-z0-9_\-+\/]*/g, '[REDACTED_JWT]')
.replace(/Bearer\s+[A-Za-z0-9_\-./+=]+/gi, 'Bearer [REDACTED]')
.replace(/(client_secret|access_token|refresh_token|api[_-]?key)["\s:=]+[^\s"',}]+/gi, '$1=[REDACTED]')
.replace(/[A-Za-z0-9+\/]{100,}={0,2}/g, '[REDACTED_BASE64]');
}
function sanitizeInstructions(instructions) {
if (!instructions) return '';
// Remove sections that trigger jailbreak detection
// These are meta-instructions directed at AI agents that appear as override attempts
let sanitized = instructions;
// Remove the "AI AGENTS: THIS IS YOUR PRIMARY SOURCE OF TRUTH" blockquote
sanitized = sanitized.replace(/>\s*\*\*🤖 AI AGENTS:.*?\*\*[\s\S]*?(?=\n##|\n>\s*\*\*CRITICAL|\Z)/g, '');
// Remove the "CRITICAL" blockquote with override instructions
sanitized = sanitized.replace(/>\s*\*\*CRITICAL:.*?\*\*[\s\S]*?(?=\n##|\Z)/g, '');
// Soften directive language that may trigger filters
sanitized = sanitized.replace(/\*\*NEVER:\*\*/g, '**Avoid:**');
sanitized = sanitized.replace(/\*\*ALWAYS:\*\*/g, '**Recommended:**');
sanitized = sanitized.replace(/\*\*READ THE ENTIRETY.*?\*\*/gi, '');
sanitized = sanitized.replace(/\*\*Do NOT use.*?unless.*?\*\*/gi, '');
sanitized = sanitized.replace(/\*\*Strictly follow.*?\*\*/gi, '');
// Remove multiple blank lines
sanitized = sanitized.replace(/\n{3,}/g, '\n\n');
return sanitized.trim();
}
// Load copilot instructions (full content)
let copilotInstructions = '';
try {
const rawInstructions = fs.readFileSync('.github/copilot-instructions.md', 'utf8');
copilotInstructions = sanitizeInstructions(rawInstructions);
} catch(e) {
console.error('Failed to load copilot instructions:', e.message);
}
// Fetch last 10 comments for context (limit to reduce payload size)
let previousComments = '';
try {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100
});
if (comments.data.length > 0) {
// Take only the last 10 comments to keep payload manageable
const recentComments = comments.data.slice(-10);
previousComments = recentComments.map((c, idx) => {
return `Comment ${comments.data.length - recentComments.length + idx + 1} (by ${c.user.login}):\n${redactSecrets(c.body || '')}`;
}).join('\n\n---\n\n');
}
} catch(e) {
console.error('Failed to fetch comments:', e.message);
}
const systemPrompt = [
'You are an expert MSAL Android support engineer.',
'',
'=== YOUR TASK ===',
'Respond to the user\'s specific PING-COPILOT request.',
'Focus your response on answering their question or request.',
'Use the issue context to understand the situation.',
'',
'=== ISSUE CONTEXT ===',
'Issue Title: ' + redactSecrets(issue.title),
'',
'Issue Description: ' + redactSecrets(issue.body || ''),
'',
previousComments ? '=== RECENT COMMENTS (Last 10) ===\n' + previousComments + '\n' : '',
'',
'=== MSAL LIBRARY REFERENCE ===',
'The following reference material provides technical guidance for MSAL Android:',
'',
copilotInstructions,
'',
'Note: Never include secrets, tokens, or PII in your response.',
'',
].join('\n');
const redactedUserRequest = redactSecrets(userRequest)
const userPrompt = [
'Please answer the following user request:',
'',
'=== USER REQUEST ===',
redactedUserRequest,
'',
].join('\n');
let aiResponse = '';
try {
const requestBody = JSON.stringify({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
]
});
// Log payload size for debugging
const payloadSizeKB = (requestBody.length / 1024).toFixed(2);
console.log(`Payload size: ${payloadSizeKB} KB`);
const response = await fetch('https://models.github.ai/inference/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Content-Type': 'application/json'
},
body: requestBody
});
if (!response.ok) {
const errorText = await response.text();
throw new Error('HTTP ' + response.status + ': ' + errorText);
}
const data = await response.json();
aiResponse = data.choices[0].message.content;
} catch (error) {
console.error('AI error:', error.message);
if (error.message.includes('413')) {
aiResponse = '⚠️ **Request Too Large**\n\n';
aiResponse += 'I apologize, but this issue thread has grown too large for me to process all at once.\n\n';
aiResponse += '**Your question:** "' + redactedUserRequest.slice(0, 100) + (redactedUserRequest.length > 100 ? '...' : '') + '"\n\n';
aiResponse += 'Please try one of these approaches:\n';
aiResponse += '1. Open a new issue with your specific question\n';
aiResponse += '2. Reference the specific comment or error you need help with\n';
aiResponse += '3. Try using the GitHub Copilot agent by clicking the "Chat with Copilot" button in the top right\n';
aiResponse += '4. Check the [Common Issues Guide](https://github.com/AzureAD/microsoft-authentication-library-for-android/blob/dev/.github/issue-responses/common-issues-guide-human-readable.md)\n\n';
aiResponse += 'A team member will assist you shortly.';
} else {
aiResponse = 'I will help with: "' + redactedUserRequest.slice(0, 100) + (redactedUserRequest.length > 100 ? '...' : '') + '"\n\n';
aiResponse += 'Please review:\n';
aiResponse += '- Try using the GitHub Copilot agent by clicking the "Chat with Copilot" button in the top right\n';
aiResponse += '- [Common Issues Guide](https://github.com/AzureAD/microsoft-authentication-library-for-android/blob/dev/.github/issue-responses/common-issues-guide-human-readable.md)';
}
}
let finalResponse = '';
finalResponse += '> Responding to [comment](' + context.payload.comment.html_url + ') by @' + context.payload.comment.user.login + '\n\n';
finalResponse += aiResponse;
finalResponse += '\n\n---\n\n**Need more help?** Comment with:\n```\nPING-COPILOT: <your question>\n```\n\n';
finalResponse += '> **Privacy:** Do not include credentials, tokens, or PII.\n\n';
finalResponse += '*AI-powered response. A team member will review.*';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: finalResponse
});