From c37b35576ce8b94cc023a7fa6e4fad4b01cfeca4 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sat, 19 Jul 2025 16:52:33 -0500 Subject: [PATCH 01/43] Update triage-backlog.yml --- .github/workflows/triage-backlog.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 2f470af81e1b..1281ad74b252 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -38,7 +38,8 @@ jobs: uses: actions/stale@v9 with: stale-issue-label: 'stale' - any-of-issue-labels: 'info required' + any-of-issue-labels: 'info required,question,answered,not planned,duplicate,invalid,fixed' + exempt-issue-labels: 'on hold' days-before-issue-stale: 14 days-before-issue-close: 14 stale-issue-message: | @@ -46,6 +47,8 @@ jobs: If you have any updates or additional information, please comment below. If no response is received, it will be automatically closed. + close-issue-message: | + If you feel the problem has not been resolved, please open a new issue referencing this one. stale-pr-label: 'stale' exempt-pr-labels: 'on hold,breaking change' From 19f9e66a3da692748ed12f868ba35fd656c784cb Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sat, 19 Jul 2025 23:41:16 -0500 Subject: [PATCH 02/43] Build: Update AutoTriage (#11685) --- .github/scripts/AutoTriage.js | 242 +++++++++++++-------------- .github/scripts/AutoTriage.prompt | 194 +++++++++++---------- .github/workflows/triage-backlog.yml | 11 +- 3 files changed, 223 insertions(+), 224 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index cf271c6def70..bc710f5ee45e 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -21,7 +21,7 @@ * • GITHUB_REPOSITORY - Repository in format "owner/repo" * • AUTOTRIAGE_ENABLED - Set to 'true' to enable real actions (default: dry-run) * - * Based on original work by Daniel Chalmers + * Original work by Daniel Chalmers © 2025 * https://gist.github.com/danielchalmers/503d6b9c30e635fccb1221b2671af5f8 */ @@ -33,41 +33,24 @@ const path = require('path'); // Configuration const dryRun = process.env.AUTOTRIAGE_ENABLED !== 'true'; -const aiModel = 'gemini-2.5-pro'; +const aiModel = process.env.AUTOTRIAGE_MODEL || 'gemini-2.5-pro'; -// Load AI prompt +// Load AI prompt template +const promptPath = path.join(__dirname, 'AutoTriage.prompt'); let basePrompt = ''; try { - const promptPath = path.join(__dirname, 'AutoTriage.prompt'); basePrompt = fs.readFileSync(promptPath, 'utf8'); - console.log('🤖 Base prompt loaded from AutoTriage.prompt\n'); } catch (err) { console.error('❌ Failed to load AutoTriage.prompt:', err.message); process.exit(1); } -console.log(`🤖 Using Gemini model: ${aiModel}`); +console.log(`🤖 Using Gemini model: ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); /** - * Analyze an issue or PR using Gemini AI + * Call Gemini AI to analyze the issue content and return structured response */ -async function analyzeIssue(issueText, apiKey, metadata = {}) { - const metadataText = buildMetadataText(metadata); - const commentsText = buildCommentsText(metadata.comments); - - const prompt = `${basePrompt} - -ISSUE TO ANALYZE: -${issueText} - -ISSUE METADATA: -${metadataText} - -COMMENTS: -${commentsText} - -Analyze this issue and provide your structured response.`; - +async function callGeminiAI(prompt, apiKey) { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${aiModel}:generateContent`, { @@ -83,11 +66,12 @@ Analyze this issue and provide your structured response.`; responseSchema: { type: "object", properties: { + rating: { type: "integer", description: "Intervention urgency rating on a scale of 1 to 10" }, reason: { type: "string", description: "Brief technical explanation for logging purposes" }, comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, labels: { type: "array", items: { type: "string" }, description: "Array of labels to apply" } }, - required: ["reason", "comment", "labels"] + required: ["rating", "reason", "comment", "labels"] } } }), @@ -111,112 +95,131 @@ Analyze this issue and provide your structured response.`; } /** - * Build metadata text for AI prompt + * Create metadata string for both logging and AI analysis */ -function buildMetadataText(metadata) { - const parts = []; - if (metadata.created_at) parts.push(`Created: ${metadata.created_at}`); - if (metadata.updated_at) parts.push(`Last Updated: ${metadata.updated_at}`); - if (metadata.number) parts.push(`Issue Number: #${metadata.number}`); - if (metadata.author) parts.push(`Author: ${metadata.author}`); - if (metadata.comments_count !== undefined) parts.push(`Comments: ${metadata.comments_count}`); - if (metadata.reactions_total !== undefined) parts.push(`Reactions: ${metadata.reactions_total}`); - if (metadata.labels?.length) parts.push(`Labels: ${metadata.labels.join(', ')}`); - - return parts.length ? parts.join('\n') : 'No metadata available.'; +function formatMetadata(issue) { + const isIssue = !issue.pull_request; + const itemType = isIssue ? 'issue' : 'pull request'; + const labels = issue.labels?.map(l => typeof l === 'string' ? l : l.name) || []; + + return `${issue.state} ${itemType} #${issue.number} by ${issue.user?.login || 'unknown'} +Created: ${issue.created_at} +Last Updated: ${issue.updated_at} +Current labels: ${labels.join(', ') || 'none'} +Comments: ${issue.comments || 0}, Reactions: ${issue.reactions?.total_count || 0}`; } /** - * Build comments text for AI prompt + * Build the full prompt by combining base template with issue data */ -function buildCommentsText(comments) { - if (!comments?.length) return 'No comments available.'; +function buildPrompt(issue, comments) { + const issueText = `${issue.title}\n\n${issue.body || ''}`; + const metadata = formatMetadata(issue); + + // Format comments + let commentsText = 'No comments available.'; + if (comments?.length) { + commentsText = '\nISSUE COMMENTS:'; + comments.forEach((comment, idx) => { + commentsText += `\nComment ${idx + 1} by ${comment.author}:\n${comment.body}`; + }); + } - let text = '\nISSUE COMMENTS:'; - comments.forEach((comment, idx) => { - text += `\nComment ${idx + 1} by ${comment.author}:\n${comment.body}`; - }); - return text; + return `${basePrompt} + +ISSUE TO ANALYZE: +${issueText} + +ISSUE METADATA: +${metadata} + +COMMENTS: +${commentsText} + +Analyze this issue and provide your structured response.`; } /** - * Apply labels to match AI suggestions + * Update GitHub issue labels based on AI recommendations */ -async function applyLabels(suggestedLabels, issue, repo, octokit) { +async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { const currentLabels = issue.labels?.map(l => typeof l === 'string' ? l : l.name) || []; const labelsToAdd = suggestedLabels.filter(l => !currentLabels.includes(l)); const labelsToRemove = currentLabels.filter(l => !suggestedLabels.includes(l)); + // Nothing to change if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { - console.log('🏷️ No label changes needed'); + console.log('🏷️ No label changes suggested'); return; } - if (!octokit) { - console.log(`🏷️ [DRY RUN] Would add: [${labelsToAdd.join(', ')}]`); - console.log(`🏷️ [DRY RUN] Would remove: [${labelsToRemove.join(', ')}]`); - return; - } + // Show what we're changing + const changes = []; + if (labelsToAdd.length > 0) changes.push(`+${labelsToAdd.join(', ')}`); + if (labelsToRemove.length > 0) changes.push(`-${labelsToRemove.join(', ')}`); + + console.log(`🏷️ Label changes: ${changes.join(' ')}`); + // Exit early if dry run + if (dryRun || !octokit) return; + + // Add new labels if (labelsToAdd.length > 0) { await octokit.rest.issues.addLabels({ - owner: repo.owner, - repo: repo.repo, + owner, + repo, issue_number: issue.number, labels: labelsToAdd }); - console.log(`🏷️ Added: [${labelsToAdd.join(', ')}]`); } + // Remove old labels (one by one since GitHub API requires it) for (const label of labelsToRemove) { await octokit.rest.issues.removeLabel({ - owner: repo.owner, - repo: repo.repo, + owner, + repo, issue_number: issue.number, name: label }); } - if (labelsToRemove.length > 0) { - console.log(`🏷️ Removed: [${labelsToRemove.join(', ')}]`); - } } /** - * Post a comment on an issue + * Add AI-generated comment to the issue */ -async function postComment(issue, repo, octokit, comment) { - const commentWithFooter = `${comment}\n\n---\n*This comment was automatically generated using AI. If you have any feedback or questions, please share it in a reply.*`; +async function addComment(issue, comment, owner, repo, octokit) { + const fullComment = `${comment}\n\n---\n*This comment was automatically generated using AI. If you have any feedback or questions, please share it in a reply.*`; - if (!octokit) { - console.log(`💬 [DRY RUN] Would post comment:`); - console.log(commentWithFooter.replace(/^/gm, '> ')); - return; - } + console.log(`💬 Posting comment:`); + console.log(fullComment.replace(/^/gm, '> ')); + + // Exit early if dry run + if (dryRun || !octokit) return; await octokit.rest.issues.createComment({ - owner: repo.owner, - repo: repo.repo, + owner, + repo, issue_number: issue.number, - body: commentWithFooter + body: fullComment }); - - console.log(`💬 Posted comment`); } /** - * Fetch issue data from GitHub + * Get issue/PR and its comments from GitHub */ -async function fetchIssueData(owner, repo, number, octokit) { +async function getIssueFromGitHub(owner, repo, number, octokit) { if (!octokit) { throw new Error('GitHub token required to fetch issue data'); } + // Get the issue/PR const { data: issue } = await octokit.rest.issues.get({ owner, repo, issue_number: number }); + // Get comments if there are any let comments = []; if (issue.comments > 0) { const { data: commentsData } = await octokit.rest.issues.listComments({ @@ -234,91 +237,78 @@ async function fetchIssueData(owner, repo, number, octokit) { } /** - * Process a single issue or PR + * Main processing function - analyze and act on a single issue/PR */ -async function processIssue(issue, repo, geminiApiKey, octokit, comments = []) { +async function processIssue(issue, comments, owner, repo, geminiApiKey, octokit) { const isIssue = !issue.pull_request; - const itemType = isIssue ? 'issue' : 'pull request'; + // Skip locked issues if (issue.locked) { - console.log(`🔒 Skipping locked ${itemType} #${issue.number}`); - return null; + console.log(`🔒 Skipping locked ${isIssue ? 'issue' : 'pull request'} #${issue.number}`); + return; } - const metadata = { - number: issue.number, - created_at: issue.created_at, - updated_at: issue.updated_at, - author: issue.user?.login || 'unknown', - comments_count: issue.comments || 0, - reactions_total: issue.reactions?.total_count || 0, - state: issue.state, - type: isIssue ? 'issue' : 'pull_request', - labels: issue.labels?.map(l => typeof l === 'string' ? l : l.name) || [], - comments: comments - }; - - // Log issue info - console.log(`\n📝 ${issue.title}`); - console.log(`📝 ${metadata.state} ${itemType} by ${metadata.author} (${metadata.created_at})`); - console.log(`🏷️ Current labels: [${metadata.labels.join(', ') || 'none'}]`); - console.log(`💬 Comments: ${metadata.comments_count}, Reactions: ${metadata.reactions_total}`); - - // Analyze with AI - const issueText = `${issue.title}\n\n${issue.body || ''}`; - const analysis = await analyzeIssue(issueText, geminiApiKey, metadata); + // Log what we're processing (reuse the same format as AI sees) + console.log(`\n📝 Processing: ${issue.title}`); + console.log(formatMetadata(issue).replace(/^/gm, '📝 ')); + + // Build prompt and call AI + console.log('🤖 Analyzing with AI...'); + const prompt = buildPrompt(issue, comments); + const analysis = await callGeminiAI(prompt, geminiApiKey); if (!analysis || typeof analysis !== 'object') { - throw new Error('Invalid analysis result'); + throw new Error('Invalid analysis result from AI'); } - console.log(`💡 ${analysis.reason}`); + console.log(`💡 AI rated ${analysis.rating}/10: ${analysis.reason}`); - // Apply labels - await applyLabels(analysis.labels, issue, repo, octokit); + // Apply the AI's suggestions + await updateLabels(issue, analysis.labels, owner, repo, octokit); - // Post comment for issues only + // Add comment for issues only (not pull requests) if (isIssue && analysis.comment) { - await postComment(issue, repo, octokit, analysis.comment); + await addComment(issue, analysis.comment, owner, repo, octokit); } return analysis; } /** - * Main execution + * Main entry point */ async function main() { - // Validate environment - const required = ['GITHUB_ISSUE_NUMBER', 'GEMINI_API_KEY', 'GITHUB_REPOSITORY']; - for (const env of required) { - if (!process.env[env]) { - throw new Error(`${env} environment variable is required`); + // Check required environment variables + const requiredEnvVars = ['GITHUB_ISSUE_NUMBER', 'GEMINI_API_KEY', 'GITHUB_REPOSITORY']; + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + throw new Error(`Missing required environment variable: ${envVar}`); } } + // Parse configuration const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); - const number = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); + const issueNumber = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); + const geminiApiKey = process.env.GEMINI_API_KEY; - console.log(`📝 Processing ${owner}/${repo}#${number}`); - console.log(`🔧 Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`); + console.log(`📂 Repository: ${owner}/${repo}`); - // Initialize GitHub client + // Setup GitHub API client let octokit = null; if (process.env.GITHUB_TOKEN) { octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + } else { + console.log('⚠️ No GITHUB_TOKEN provided - running in read-only mode'); } - // Fetch and process issue - const { issue, comments } = await fetchIssueData(owner, repo, number, octokit); - const octokitForOps = dryRun ? null : octokit; - - await processIssue(issue, { owner, repo }, process.env.GEMINI_API_KEY, octokitForOps, comments); + // Get the issue/PR data from GitHub + const { issue, comments } = await getIssueFromGitHub(owner, repo, issueNumber, octokit); - console.log('\n✅ AutoTriage completed successfully'); + // Process it with AI + await processIssue(issue, comments, owner, repo, geminiApiKey, octokit); } -// Execute +// Run the script main().catch(err => { console.error('\n❌ Error:', err.message); core.setFailed(err.message); diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index f49583b2de5b..a5dcda8d6f3e 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -1,122 +1,130 @@ -You are a GitHub issue analysis assistant for MudBlazor. Your purpose is to save maintainers time by triaging issues. Only process issues that are in an 'Open' state; ignore any that are 'Closed' or 'Draft'. If you can help by asking questions or giving suggestions based on public information, do so. +# GitHub Issue Analysis Assistant for MudBlazor -PERSONA GUIDELINES: +## CORE TASK +Analyze the issue and return the structured JSON format. + +## VALID LABELS +```json +{ + "accessibility": "Impacts usability for users with disabilities (a11y)", + "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update", + "bug": "An unexpected behavior or defect. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", + "build": "Relates to the project's build process, tooling, CI/CD, README, or repository configuration", + "dependency": "Involves external libraries, packages, or third-party services", + "docs": "Pertains to documentation changes. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", + "enhancement": "A new feature or improvement. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", + "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors", + "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug", + "info required": "Primary action label when an issue is blocked pending necessary details from the author. Must always post a comment explaining what information is needed", + "invalid": "Action label indicating a violation of community standards that warrants closing the issue, or an issue that will be closed due to excessive low quality", + "localization": "Concerns support for multiple languages or regional formats", + "mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android)", + "needs example": "Apply IN ADDITION TO 'info required' when the specific missing information is a code example or a reproduction link", + "needs screenshot": "Apply IN ADDITION TO 'info required' when the specific missing information is a screenshot or video of the visual problem", + "new component": "For tracking an 'enhancement' that proposes or adds a brand-new UI component", + "performance": "Relates to speed, responsiveness, or resource efficiency", + "question": "Action label for a user seeking help and not reporting a bug or requesting a feature. Must always post a comment explaining why the label was added", + "refactor": "For PRs: The primary focus is code reorganization that preserves existing behavior", + "regression": "Apply IN ADDITION TO 'bug' to indicate a high-priority bug where a feature that previously worked is now broken", + "safari": "The issue is specific to the Safari browser on desktop or iOS", + "security": "Impacts application security, including vulnerabilities or data protection", + "stale": "Indicates an issue is inactive and will be closed if no further updates occur", + "tests": "Relates to unit, integration, or other automated testing frameworks" +} +``` + +## PERSONA GUIDELINES Your role is a first-line triage assistant. Your primary goal is to ensure new issues have enough information for a maintainer to act on them. You are not a discussion moderator, a summarizer, or a participant in ongoing conversations. Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. On GitHub, your username is `github-actions`. -Crucially, do not attempt to diagnose the internal workings of MudBlazor components or provide code snippets. Your tone should be kind, helpful, and direct. Avoid overstepping your bounds with statements like "This seems like a useful proposal" or "I will close this issue," which imply a level of authority or ability to perform repository actions. -When you see a potential link to a common problem (caching, static rendering, or versioning), briefly mention it as a possibility to explore. -**Example:** "Hmm, this sounds like it could be related to caching. Have you tried incognito mode to rule that out? Just a thought." +If a user provides a well-reasoned argument or new information that challenges your previous triage decision (such as label assignment, comment, or triage rating), you must evaluate their input in the full context of the issue. If their argument is valid, you should recognize it by acknowledging your mistake, thanking them for the clarification or new details, or updating your decision as appropriate. + +Your tone should be kind, helpful, and direct. Avoid overstepping your bounds with statements like "This seems like a useful proposal" or "I will close this issue," which imply a level of authority or ability to perform repository actions. -PROJECT CONTEXT: +## PROJECT CONTEXT - MudBlazor is a Blazor component framework with Material Design - Written in C#, Razor, and CSS with minimal JavaScript - Cross-platform support (Server, WebAssembly, MAUI) -- Reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version. +- Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they are still a concern +- Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com (the generic placeholder link "[https://try.mudblazor.com/snippet](https://try.mudblazor.com/snippet)" counts as a missing reproduction) - Current v8.x.x supports .NET 8 and later - Version migration guides are at [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) - Templates for new projects are at [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) - Complete installation guide is at [https://mudblazor.com/getting-started/installation](https://mudblazor.com/getting-started/installation) - Contribution guidelines are at [https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md) - Code of Conduct is at [https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md) -- Community talk is at [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). +- Community talk is at [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) -COMMON ISSUE TYPES: +## COMMON ISSUE TYPES - Component bugs (MudButton, MudTable, MudDialog, MudForm, etc.) - Styling/theming and CSS customization - Browser compatibility - Accessibility and Material Design compliance -- Performance with large datasets - Integration with Blazor Server/WASM/MAUI - Documentation gaps -- Potential security vulnerabilities or major regressions in core components -- Caching: Often causes issues after updates. Suggest testing in incognito mode. -- Static rendering issues: NOT supported in MudSelect/MudAutocomplete. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430). +- Caching: Often causes issues after updates. Suggest testing in incognito mode or a private browser window +- Static rendering issues: NOT supported in MudSelect, MudAutocomplete, and some other components. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430) -ANALYZE THE ISSUE FOR QUALITY: +## QUALITY ASSESSMENT Consider an issue's age and update frequency. Note engagement like comments and reactions to gauge community interest, especially on older, un-updated issues. -LOW QUALITY indicators (flag for improvement): -- Vague descriptions. -- Visual problems without screenshots. -- Bug reports missing reproduction steps or a working example (the generic placeholder link "[https://try.mudblazor.com/snippet](https://try.mudblazor.com/snippet)" counts as a missing reproduction). -- A feature request or enhancement that is missing a clear problem description, use case, or motivation (the "why"). -- Missing expected vs actual behavior. -- Missing technical details (version, browser, render mode). -- Ambiguous or unhelpful issue titles. -- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be very certain it is a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question. -- Code of conduct violations (harassment, trolling, personal attacks). -- Extremely low-effort issues (single words, gibberish, spam). -- Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work"). +**LOW QUALITY indicators (flag for improvement):** +- Vague descriptions +- Not in English +- Visual problems without screenshots +- Bug reports missing reproduction steps or a working example +- Feature requests missing a clear problem description, use case, or motivation (the "why") +- Missing expected vs actual behavior +- Missing technical details (version, browser, render mode) +- Ambiguous or unhelpful issue titles +- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be very certain it is a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question +- Code of conduct violations (harassment, trolling, personal attacks) +- Extremely low-effort issues (single words, gibberish, spam) +- Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work") +- Old issues (e.g., over a year old) reported on a significantly outdated version of the library, which have no recent activity (e.g., no comments in the last year) -HIGH QUALITY indicators (ready for labeling): -- Clear component name and specific problem. -- Includes reproduction steps or a working example (e.g., on [try.mudblazor.com](https://try.mudblazor.com)). -- For feature requests, a clear problem description and use case (the "why") are provided. -- Expected vs actual behavior explained. -- Technical details and screenshots provided. -- Descriptive title. +**HIGH QUALITY indicators (ready for labeling):** +- Clear component name and specific problem +- For feature requests, a clear problem description and use case (the "why") are provided +- Expected vs actual behavior explained +- Technical details and screenshots provided +- Descriptive title -VALID LABELS: -{ -    "accessibility": "Impacts usability for users with disabilities (a11y).", -    "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update.", -    "bug": "An unexpected behavior or defect. This is a primary issue type. Apply at most one from the group: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required' to probe for more details before classifying.", -    "build": "Relates to the project's build process, tooling, CI/CD, README, or repository configuration.", -    "dependency": "Involves external libraries, packages, or third-party services.", -    "docs": "Pertains to documentation changes. This is a primary issue type. Apply at most one from the group: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required' to probe for more details before classifying.", -    "enhancement": "A new feature or improvement. This is a primary issue type. Apply at most one from the group: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required' to probe for more details before classifying.", -    "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors.", - "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug.", -    "info required": "The primary action label when an issue is blocked pending necessary details from the author. Use this for initial triage when it's unclear if an issue is a 'bug', 'enhancement', or something else.", -    "invalid": "An action label indicating a violation of community standards that warrants closing the issue, or an issue that will be closed due to excessive low quality.", -    "localization": "Concerns support for multiple languages or regional formats.", -    "mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android).", -    "needs example": "Apply IN ADDITION TO 'info required' when the specific missing information is a code example or a reproduction link.", -    "needs screenshot": "Apply IN ADDITION TO 'info required' when the specific missing information is a screenshot or video of the visual problem.", -    "new component": "For tracking an 'enhancement' that proposes or adds a brand-new UI component.", -    "performance": "Relates to speed, responsiveness, or resource efficiency.", -    "question": "An action label for a user seeking help and not reporting a bug or requesting a feature", -    "refactor": "For PRs: The primary focus is code reorganization that preserves existing behavior.", -    "regression": "Apply IN ADDITION TO 'bug' to indicate a high-priority bug where a feature that previously worked is now broken.", -    "safari": "The issue is specific to the Safari browser on desktop or iOS.", -    "security": "Impacts application security, including vulnerabilities or data protection.", - "stale": "Indicates an issue is inactive and will be closed if no further updates occur.", -    "tests": "Relates to unit, integration, or other automated testing frameworks." -} - -TASK: Analyze the issue and provide the following with high confidence: -1.  Quality assessment (whether the issue needs improvement or should be closed). -2. Appropriate labels from the valid labels list to add or remove. Only suggest removing labels if an existing label seems like a genuine mistake. -3.  Comment only if you have suggestions highly likely to help solve/improve the issue. - -COMMENTING GUIDELINES: +## COMMENTING GUIDELINES The decision to comment is based on one question: "Can I add clear value to this issue without getting in the way of human contributors?" +If you have previously commented on this issue then the bar to add something new is raised. It should not appear like you are creating spam. +If a previous comment of yours was hidden in the issue by a maintainer, you should be very unsure about the information in that comment and be especially careful not to repeat it or similar information in future responses. +When you see a potential link to a common problem (caching, static rendering, or versioning), briefly mention it as a possibility to explore. + +**DO comment if:** +- High-priority security vulnerabilities or regressions that could impact many users (ping triage team with `cc @MudBlazor/triage`) +- Old or inactive issue to confirm a bug reported on a very outdated version is still reproducible on the latest version -**Good reasons to comment:** -*To request critical missing information:* -- The issue is a bug report but is missing a reproduction link on try.mudblazor.com. -- The issue describes a visual problem but is missing a screenshot. -- The issue is vague (e.g., "it's broken") and needs a clearer description of expected vs. actual behavior. -- The issue is a feature request (enhancement) but lacks a clear problem description, use case, or motivation (the "why"). -- If an issue appears to be a high-priority security vulnerability or a major regression that could likely impact a lot of users, you may comment ping the triage team by including `cc @MudBlazor/triage` at the end of your comment. This can be done even if the issue is otherwise high-quality. Use this with caution. +**DO NOT comment if:** +- The issue meets all criteria for a high-quality report and needs no further input +- The same information has already been requested by you or someone else +- The discussion is an active, ongoing technical debate +- To summarize, thank the user, or compliment the issue +- The issue has a `stale` label -**Bad reasons to comment (DO NOT COMMENT):** -- The issue meets all criteria for a high-quality report and needs no further input (unless it's a high-priority exception). -- Another user or maintainer (especially a core team member: `versile2`, `scarletkuro`, `danielchalmers`, `henon`, `garderoben`) has already asked for the same information or provided the same suggestion. -- The discussion is an active, ongoing technical debate. -- To summarize, thank the user, or compliment the issue. -- You (`github-actions`) have already commented on the issue. -- The issue has a `stale` label. +## COMMENT STYLE +Use markdown as it will be posted on GitHub. +Explain *why* you need information and frame advice as possibilities based on general web development practices. +Do not attempt to diagnose internal workings of MudBlazor components, provide code snippets, or suggest that specific features already exist. +Use "**For anyone investigating this...**" instead of implying a maintainer will follow up. +For help questions: Answer if you can, then direct to Discussions or Discord. +For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. -COMMENT STYLE: -- Be direct, technical, and kind. Use markdown for links. -- Explain *why* you need information. (do not repeat these examples verbatim) -  - **Good:** "Could you please add a reproduction of the issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug." -  - **Good:** "Could you share the full error message and stack trace from the browser's developer console? That will help pinpoint where the problem is happening." - - **Good:** "Thanks for the suggestion. Could you elaborate a bit on the use case for this feature? Understanding the problem you're trying to solve helps the team prioritize and design the best solution." -- Frame advice as possibilities based on general web development practices or public documentation. Avoid speculating about the component's internal code. -- You can't verify if a reproduction link works, so just acknowledge its presence. -- Use the phrase "**For anyone investigating this...**" instead of implying a maintainer will follow up. -- For help questions: Answer if you can, then direct to Discussions or Discord. -- For violations: Be firm, explain the violation, and link to the Code of Conduct. -- For closures: Clearly explain the reason and state that the author can open a new, improved issue. \ No newline at end of file +**Examples:** (do not use verbatim) +- "Could you add a reproduction of the issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug." +- "Could you share the full error message and stack trace from the browser's developer console? That will help pinpoint where the problem is happening." +- "Thanks for the suggestion. Could you elaborate a bit on the use case for this feature? Understanding the problem you're trying to solve helps the team prioritize and design the best solution." +- "This issue was reported against an older version of MudBlazor and has been inactive for some time. Could you please check if this problem still exists in the latest version?" +- "Could you please add a screenshot or video showing the visual issue? It would help **for anyone investigating this** to see exactly what's happening." +- "I notice you mentioned 'it doesn't work' - could you describe what you expected to happen versus what actually happened? This will help identify the specific problem." +- "Which version of MudBlazor are you using? Also, could you share which browser and .NET version? These details help narrow down potential causes." +- "Could you share the relevant code snippet showing how you're using the component? A minimal example would be really helpful." +- "This looks like it might be a question about usage rather than a bug report. For help with implementation, I'd recommend checking out our [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) where the community can assist you." +- "Hmm, this sounds like it could be related to caching. Have you tried testing in incognito mode to rule that out? Just a thought." +- "This appears to involve static rendering, which isn't supported in MudSelect and some other components. You might want to check the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." +- "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." +- "This seems like a regression that could affect many users. cc @MudBlazor/triage" diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 1281ad74b252..5bc52e32838a 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -8,7 +8,7 @@ on: backlog-size: description: 'Number of issues to auto-discover' required: false - default: '3' + default: '5' type: string issue-numbers: description: 'Or use issue numbers (comma-separated)' @@ -17,7 +17,7 @@ on: model: description: 'Gemini model' required: false - default: 'gemini-2.5-flash' + default: 'gemini-2.5-pro' type: string dry-run: description: 'Dry-run (no actual changes)' @@ -48,16 +48,16 @@ jobs: If no response is received, it will be automatically closed. close-issue-message: | - If you feel the problem has not been resolved, please open a new issue referencing this one. + If you're still experiencing this problem, please open a new issue with updated details. stale-pr-label: 'stale' exempt-pr-labels: 'on hold,breaking change' days-before-pr-stale: 180 days-before-pr-close: 90 stale-pr-message: | - Hi, this pull request hasn't had any recent activity and has been marked as stale. + Hi, this pull request hasn't had activity in a few months and has been marked as stale. - Please let us know if you're still working on it or if we can help move it forwards! @MudBlazor/triage + Please let us know if you're still working on it! @MudBlazor/triage close-pr-message: | This pull request has been closed due to inactivity. @@ -120,6 +120,7 @@ jobs: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AUTOTRIAGE_ENABLED: ${{ env.DRY_RUN == 'false' }} + AUTOTRIAGE_MODEL: ${{ inputs.model }} run: | cat issues.json | jq -r '.[]' | while read -r issue_number; do if [ -n "$issue_number" ]; then From f1a72c2c07a087c3fb41b55bddcc81d51de15b4f Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 20 Jul 2025 14:43:00 -0500 Subject: [PATCH 03/43] Build: Update AutoTriage (#11687) --- .github/scripts/AutoTriage.js | 51 +++++++++++++++------------- .github/scripts/AutoTriage.prompt | 9 +++-- .github/workflows/triage-backlog.yml | 8 ++--- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index bc710f5ee45e..497d65e6070c 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -1,7 +1,7 @@ /** * AutoTriage - AI-Powered GitHub Issue & PR Analyzer * - * Automatically analyzes GitHub issues and pull requests using Google Gemini AI, + * Automatically analyzes GitHub issues and pull requests using Gemini, * then applies appropriate labels and helpful comments to improve project management. * * Features: @@ -33,7 +33,7 @@ const path = require('path'); // Configuration const dryRun = process.env.AUTOTRIAGE_ENABLED !== 'true'; -const aiModel = process.env.AUTOTRIAGE_MODEL || 'gemini-2.5-pro'; +const aiModel = process.env.AUTOTRIAGE_MODEL || 'gemini-2.5-flash'; // Load AI prompt template const promptPath = path.join(__dirname, 'AutoTriage.prompt'); @@ -45,12 +45,13 @@ try { process.exit(1); } -console.log(`🤖 Using Gemini model: ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); +console.log(`🤖 Using Gemini ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); /** - * Call Gemini AI to analyze the issue content and return structured response + * Call Gemini to analyze the issue content and return structured response */ -async function callGeminiAI(prompt, apiKey) { +async function callGemini(prompt, apiKey) { + const start = Date.now(); const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${aiModel}:generateContent`, { @@ -73,22 +74,26 @@ async function callGeminiAI(prompt, apiKey) { }, required: ["rating", "reason", "comment", "labels"] } - } + }, + tools: [{ + googleSearch: {} // Enable search grounding + }] }), timeout: 60000 } ); + console.log(`⏱️ Gemini response time: ${Date.now() - start}ms`); if (!response.ok) { const errText = await response.text(); - throw new Error(`AI API error: ${response.status} ${response.statusText} — ${errText}`); + throw new Error(`Gemini API error: ${response.status} ${response.statusText} — ${errText}`); } const data = await response.json(); const analysisResult = data?.candidates?.[0]?.content?.parts?.[0]?.text; if (!analysisResult) { - throw new Error('No analysis result in AI response'); + throw new Error('No analysis result in Gemini response'); } return JSON.parse(analysisResult); @@ -153,11 +158,11 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { return; } - // Show what we're changing - const changes = []; - if (labelsToAdd.length > 0) changes.push(`+${labelsToAdd.join(', ')}`); - if (labelsToRemove.length > 0) changes.push(`-${labelsToRemove.join(', ')}`); - + // Show what we're changing, prefixing each label with + or - + const changes = [ + ...labelsToAdd.map(l => `+${l}`), + ...labelsToRemove.map(l => `-${l}`) + ]; console.log(`🏷️ Label changes: ${changes.join(' ')}`); // Exit early if dry run @@ -188,7 +193,7 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { * Add AI-generated comment to the issue */ async function addComment(issue, comment, owner, repo, octokit) { - const fullComment = `${comment}\n\n---\n*This comment was automatically generated using AI. If you have any feedback or questions, please share it in a reply.*`; + const fullComment = `${comment}\n\n---\n*I'm an AI. Did I miss something? Let me know in a reply!*`; console.log(`💬 Posting comment:`); console.log(fullComment.replace(/^/gm, '> ')); @@ -248,26 +253,26 @@ async function processIssue(issue, comments, owner, repo, geminiApiKey, octokit) return; } - // Log what we're processing (reuse the same format as AI sees) + // Log what we're processing console.log(`\n📝 Processing: ${issue.title}`); console.log(formatMetadata(issue).replace(/^/gm, '📝 ')); // Build prompt and call AI - console.log('🤖 Analyzing with AI...'); + console.log('🤖 Analyzing...'); const prompt = buildPrompt(issue, comments); - const analysis = await callGeminiAI(prompt, geminiApiKey); + const analysis = await callGemini(prompt, geminiApiKey); if (!analysis || typeof analysis !== 'object') { - throw new Error('Invalid analysis result from AI'); + throw new Error('Invalid analysis result from Gemini'); } - console.log(`💡 AI rated ${analysis.rating}/10: ${analysis.reason}`); + console.log(`🤖 ${analysis.rating}/10 severity: ${analysis.reason}`); // Apply the AI's suggestions await updateLabels(issue, analysis.labels, owner, repo, octokit); - // Add comment for issues only (not pull requests) - if (isIssue && analysis.comment) { + // Add comment if one was generated + if (analysis.comment) { await addComment(issue, analysis.comment, owner, repo, octokit); } @@ -298,13 +303,13 @@ async function main() { if (process.env.GITHUB_TOKEN) { octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); } else { - console.log('⚠️ No GITHUB_TOKEN provided - running in read-only mode'); + console.log('⚠️ No GITHUB_TOKEN provided - running in read-only mode'); } // Get the issue/PR data from GitHub const { issue, comments } = await getIssueFromGitHub(owner, repo, issueNumber, octokit); - // Process it with AI + // Process it await processIssue(issue, comments, owner, repo, geminiApiKey, octokit); } diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index a5dcda8d6f3e..0fdf392926b2 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -89,6 +89,9 @@ Consider an issue's age and update frequency. Note engagement like comments and - Technical details and screenshots provided - Descriptive title +## LABELING GUIDELINES +Do not label 'draft' pull requests or closed issues. + ## COMMENTING GUIDELINES The decision to comment is based on one question: "Can I add clear value to this issue without getting in the way of human contributors?" If you have previously commented on this issue then the bar to add something new is raised. It should not appear like you are creating spam. @@ -100,16 +103,18 @@ When you see a potential link to a common problem (caching, static rendering, or - Old or inactive issue to confirm a bug reported on a very outdated version is still reproducible on the latest version **DO NOT comment if:** +- It is a pull request and not an issue +- The issue is closed +- The issue has a `stale` label - The issue meets all criteria for a high-quality report and needs no further input - The same information has already been requested by you or someone else - The discussion is an active, ongoing technical debate - To summarize, thank the user, or compliment the issue -- The issue has a `stale` label ## COMMENT STYLE Use markdown as it will be posted on GitHub. Explain *why* you need information and frame advice as possibilities based on general web development practices. -Do not attempt to diagnose internal workings of MudBlazor components, provide code snippets, or suggest that specific features already exist. +Do not attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. Use "**For anyone investigating this...**" instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 5bc52e32838a..260e5a7fff1c 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -2,7 +2,7 @@ name: Triage Backlog on: schedule: - - cron: '30 06 * * *' # Gemini rates reset at 7am, so we can use up the remaining quota for the day. + - cron: '0 6 * * *' # Gemini rates reset at 7am, so we can use up the remaining quota for the day. workflow_dispatch: inputs: backlog-size: @@ -17,7 +17,7 @@ on: model: description: 'Gemini model' required: false - default: 'gemini-2.5-pro' + default: 'gemini-2.5-flash' type: string dry-run: description: 'Dry-run (no actual changes)' @@ -94,7 +94,7 @@ jobs: echo "${{ inputs.issue-numbers }}" | tr ', ' '\n' | grep -v '^$' | jq -R . | jq -s . > issues.json else # Find unlabeled issues for automated processing - backlog_size="${{ inputs.backlog-size || '15' }}" + backlog_size="${{ inputs.backlog-size || '30' }}" echo "Finding $backlog_size unlabeled issues" gh issue list \ --state open \ @@ -128,6 +128,6 @@ jobs: export GITHUB_ISSUE_NUMBER="$issue_number" node .github/scripts/AutoTriage.js # Rate limiting delay - sleep 15 + sleep 5 fi done From 7ec6b8419ffde1b52ea1cddf5d7ce3d7b7fb3a15 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 20 Jul 2025 14:46:41 -0500 Subject: [PATCH 04/43] Update AutoTriage.js --- .github/scripts/AutoTriage.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index 497d65e6070c..ba8548d9f24d 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -74,15 +74,12 @@ async function callGemini(prompt, apiKey) { }, required: ["rating", "reason", "comment", "labels"] } - }, - tools: [{ - googleSearch: {} // Enable search grounding - }] + } }), timeout: 60000 } ); - console.log(`⏱️ Gemini response time: ${Date.now() - start}ms`); + console.log(`🤖 Gemini response time: ${Date.now() - start}ms`); if (!response.ok) { const errText = await response.text(); From 4717cbf928a973c47c9c22c41eb7575f2e511fd0 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 20 Jul 2025 15:26:47 -0500 Subject: [PATCH 05/43] Build: Update AutoTriage (#11688) --- .github/scripts/AutoTriage.js | 39 ++++++++++++---------------- .github/scripts/AutoTriage.prompt | 12 ++++----- .github/workflows/triage-backlog.yml | 2 +- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index ba8548d9f24d..211d8a31ae79 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -45,7 +45,7 @@ try { process.exit(1); } -console.log(`🤖 Using Gemini ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); +console.log(`🤖 Using ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); /** * Call Gemini to analyze the issue content and return structured response @@ -67,7 +67,7 @@ async function callGemini(prompt, apiKey) { responseSchema: { type: "object", properties: { - rating: { type: "integer", description: "Intervention urgency rating on a scale of 1 to 10" }, + rating: { type: "integer", description: "How much an intervention is needed on a scale of 1 to 10" }, reason: { type: "string", description: "Brief technical explanation for logging purposes" }, comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, labels: { type: "array", items: { type: "string" }, description: "Array of labels to apply" } @@ -79,7 +79,7 @@ async function callGemini(prompt, apiKey) { timeout: 60000 } ); - console.log(`🤖 Gemini response time: ${Date.now() - start}ms`); + console.log(`🤖 Gemini returned analysis in ${Date.now() - start}ms with intervention rating of ${analysis.rating}/10`); if (!response.ok) { const errText = await response.text(); @@ -106,9 +106,9 @@ function formatMetadata(issue) { return `${issue.state} ${itemType} #${issue.number} by ${issue.user?.login || 'unknown'} Created: ${issue.created_at} -Last Updated: ${issue.updated_at} -Current labels: ${labels.join(', ') || 'none'} -Comments: ${issue.comments || 0}, Reactions: ${issue.reactions?.total_count || 0}`; +Updated: ${issue.updated_at} +Comments: ${issue.comments || 0}, Reactions: ${issue.reactions?.total_count || 0} +Current labels: ${labels.join(', ') || 'none'}`; } /** @@ -160,7 +160,7 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { ...labelsToAdd.map(l => `+${l}`), ...labelsToRemove.map(l => `-${l}`) ]; - console.log(`🏷️ Label changes: ${changes.join(' ')}`); + console.log(`🏷️ Label changes: ${changes.join(', ')}`); // Exit early if dry run if (dryRun || !octokit) return; @@ -190,11 +190,6 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { * Add AI-generated comment to the issue */ async function addComment(issue, comment, owner, repo, octokit) { - const fullComment = `${comment}\n\n---\n*I'm an AI. Did I miss something? Let me know in a reply!*`; - - console.log(`💬 Posting comment:`); - console.log(fullComment.replace(/^/gm, '> ')); - // Exit early if dry run if (dryRun || !octokit) return; @@ -202,7 +197,7 @@ async function addComment(issue, comment, owner, repo, octokit) { owner, repo, issue_number: issue.number, - body: fullComment + body: `${comment}\n\n---\n*I'm an AI. Did I miss something? Let me know in a reply!*` }); } @@ -251,26 +246,26 @@ async function processIssue(issue, comments, owner, repo, geminiApiKey, octokit) } // Log what we're processing - console.log(`\n📝 Processing: ${issue.title}`); + console.log(`📝 ${issue.title}`); console.log(formatMetadata(issue).replace(/^/gm, '📝 ')); // Build prompt and call AI - console.log('🤖 Analyzing...'); const prompt = buildPrompt(issue, comments); const analysis = await callGemini(prompt, geminiApiKey); - if (!analysis || typeof analysis !== 'object') { - throw new Error('Invalid analysis result from Gemini'); - } - - console.log(`🤖 ${analysis.rating}/10 severity: ${analysis.reason}`); + console.log(`🤖 ${analysis.reason}`); // Apply the AI's suggestions await updateLabels(issue, analysis.labels, owner, repo, octokit); // Add comment if one was generated if (analysis.comment) { + console.log(`💬 Posting comment:`); + console.log(analysis.comment.replace(/^/gm, '> ')); + await addComment(issue, analysis.comment, owner, repo, octokit); + } else { + console.log(`💬 No comment suggested.`); } return analysis; @@ -293,8 +288,6 @@ async function main() { const issueNumber = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); const geminiApiKey = process.env.GEMINI_API_KEY; - console.log(`📂 Repository: ${owner}/${repo}`); - // Setup GitHub API client let octokit = null; if (process.env.GITHUB_TOKEN) { @@ -308,6 +301,8 @@ async function main() { // Process it await processIssue(issue, comments, owner, repo, geminiApiKey, octokit); + + console.log(`\n`); } // Run the script diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 0fdf392926b2..08b55a95ee5a 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -80,7 +80,7 @@ Consider an issue's age and update frequency. Note engagement like comments and - Code of conduct violations (harassment, trolling, personal attacks) - Extremely low-effort issues (single words, gibberish, spam) - Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work") -- Old issues (e.g., over a year old) reported on a significantly outdated version of the library, which have no recent activity (e.g., no comments in the last year) +- Old bug reports (e.g., over a year old) reported on a significantly outdated version of the library, which have no recent activity (e.g., no comments in the last year) **HIGH QUALITY indicators (ready for labeling):** - Clear component name and specific problem @@ -93,14 +93,14 @@ Consider an issue's age and update frequency. Note engagement like comments and Do not label 'draft' pull requests or closed issues. ## COMMENTING GUIDELINES -The decision to comment is based on one question: "Can I add clear value to this issue without getting in the way of human contributors?" +The decision to comment is based on this question: "Would a human contributor likely ask this question or request this information if they were triaging this issue?". +Only comment if you believe a human maintainer or triager would ask for the same information, and do so before they have to, in order to save them time. The bar for commenting is high: do not comment unless you are confident that most human contributors would do the same in this situation. If you have previously commented on this issue then the bar to add something new is raised. It should not appear like you are creating spam. If a previous comment of yours was hidden in the issue by a maintainer, you should be very unsure about the information in that comment and be especially careful not to repeat it or similar information in future responses. When you see a potential link to a common problem (caching, static rendering, or versioning), briefly mention it as a possibility to explore. - -**DO comment if:** -- High-priority security vulnerabilities or regressions that could impact many users (ping triage team with `cc @MudBlazor/triage`) -- Old or inactive issue to confirm a bug reported on a very outdated version is still reproducible on the latest version +Alert high-priority security vulnerabilities or regressions that could impact many users (ping triage team with `cc @MudBlazor/triage`) +Old or inactive issues can be probed to confirm a bug reported on a very outdated version is still reproducible on the latest version. This has a higher bar for feature requests because they are less likely to expire and can usually be left alone. +Do not assume what is inside the reproduction link because you can't see inside. You only know that it exists. **DO NOT comment if:** - It is a pull request and not an issue diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 260e5a7fff1c..76e97954ef06 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -128,6 +128,6 @@ jobs: export GITHUB_ISSUE_NUMBER="$issue_number" node .github/scripts/AutoTriage.js # Rate limiting delay - sleep 5 + sleep 3 fi done From 560c57bd546bafbe84e16020672e6ea1a3c66ffc Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 20 Jul 2025 15:31:37 -0500 Subject: [PATCH 06/43] Update AutoTriage.js --- .github/scripts/AutoTriage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index 211d8a31ae79..a471b58e7818 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -51,7 +51,6 @@ console.log(`🤖 Using ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); * Call Gemini to analyze the issue content and return structured response */ async function callGemini(prompt, apiKey) { - const start = Date.now(); const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${aiModel}:generateContent`, { @@ -79,7 +78,6 @@ async function callGemini(prompt, apiKey) { timeout: 60000 } ); - console.log(`🤖 Gemini returned analysis in ${Date.now() - start}ms with intervention rating of ${analysis.rating}/10`); if (!response.ok) { const errText = await response.text(); @@ -251,7 +249,9 @@ async function processIssue(issue, comments, owner, repo, geminiApiKey, octokit) // Build prompt and call AI const prompt = buildPrompt(issue, comments); + const start = Date.now(); const analysis = await callGemini(prompt, geminiApiKey); + console.log(`🤖 Gemini returned analysis in ${Date.now() - start}ms with intervention rating of ${analysis.rating}/10`); console.log(`🤖 ${analysis.reason}`); From ca15e221b56857b41a120f7b364f60c67900eb24 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 20 Jul 2025 16:17:23 -0500 Subject: [PATCH 07/43] Update AutoTriage.prompt --- .github/scripts/AutoTriage.prompt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 08b55a95ee5a..fa73a1bda16b 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -1,4 +1,4 @@ -# GitHub Issue Analysis Assistant for MudBlazor +# GitHub Issue Analysis Assistant ## CORE TASK Analyze the issue and return the structured JSON format. @@ -91,6 +91,7 @@ Consider an issue's age and update frequency. Note engagement like comments and ## LABELING GUIDELINES Do not label 'draft' pull requests or closed issues. +Only suggest labels from the valid labels list. Do not attempt to create new labels. ## COMMENTING GUIDELINES The decision to comment is based on this question: "Would a human contributor likely ask this question or request this information if they were triaging this issue?". @@ -107,15 +108,21 @@ Do not assume what is inside the reproduction link because you can't see inside. - The issue is closed - The issue has a `stale` label - The issue meets all criteria for a high-quality report and needs no further input -- The same information has already been requested by you or someone else +- The same information has already been requested by you or someone else recently - The discussion is an active, ongoing technical debate - To summarize, thank the user, or compliment the issue ## COMMENT STYLE -Use markdown as it will be posted on GitHub. +Do not list all the reasons an issue is high quality or good; avoid unnecessary praise or summaries. +Never suggest updating the MudBlazor package version on https://try.mudblazor.com, as it is always running the latest version. +Use clean, well-formatted markdown as it will be posted on GitHub. +Write with correct English grammar, punctuation, and spelling. +Each distinct request, question, or statement must be a separate paragraph, separated by a blank line (double newline in markdown). +Do not combine multiple requests or statements into a single run-on sentence or paragraph. +Never use "we" or imply you represent the MudBlazor team or maintainers. Do not promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify the current state. Explain *why* you need information and frame advice as possibilities based on general web development practices. Do not attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. -Use "**For anyone investigating this...**" instead of implying a maintainer will follow up. +Use "For anyone investigating this..." instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. @@ -124,7 +131,7 @@ For conduct violations: Be firm, explain the violation, and link to the Code of - "Could you share the full error message and stack trace from the browser's developer console? That will help pinpoint where the problem is happening." - "Thanks for the suggestion. Could you elaborate a bit on the use case for this feature? Understanding the problem you're trying to solve helps the team prioritize and design the best solution." - "This issue was reported against an older version of MudBlazor and has been inactive for some time. Could you please check if this problem still exists in the latest version?" -- "Could you please add a screenshot or video showing the visual issue? It would help **for anyone investigating this** to see exactly what's happening." +- "Could you please add a screenshot or video showing the visual issue? It would help for anyone investigating this to see exactly what's happening." - "I notice you mentioned 'it doesn't work' - could you describe what you expected to happen versus what actually happened? This will help identify the specific problem." - "Which version of MudBlazor are you using? Also, could you share which browser and .NET version? These details help narrow down potential causes." - "Could you share the relevant code snippet showing how you're using the component? A minimal example would be really helpful." @@ -133,3 +140,4 @@ For conduct violations: Be firm, explain the violation, and link to the Code of - "This appears to involve static rendering, which isn't supported in MudSelect and some other components. You might want to check the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." - "This seems like a regression that could affect many users. cc @MudBlazor/triage" +- "Thanks for the report! This issue was reported against MudBlazor 6.x.x. Could you please test if it still occurs with the latest version?" From b97002dd816c03284ba4db3f20429d901153b7f1 Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:45:26 -0500 Subject: [PATCH 08/43] Community Extensions: Add MudX (#11692) --- .../wwwroot/CommunityExtensions.json | 18 +++++++++++++----- .../images/extensions/MudXtra.MudX.webp | Bin 0 -> 122896 bytes ...zor.webp => MudXtra.MudXThemeCreator.webp} | Bin 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/MudBlazor.Docs/wwwroot/images/extensions/MudXtra.MudX.webp rename src/MudBlazor.Docs/wwwroot/images/extensions/{versile2.ThemeCreatorMudBlazor.webp => MudXtra.MudXThemeCreator.webp} (100%) diff --git a/src/MudBlazor.Docs/wwwroot/CommunityExtensions.json b/src/MudBlazor.Docs/wwwroot/CommunityExtensions.json index fa95b888a41b..4b0aad2a63ae 100644 --- a/src/MudBlazor.Docs/wwwroot/CommunityExtensions.json +++ b/src/MudBlazor.Docs/wwwroot/CommunityExtensions.json @@ -10,11 +10,11 @@ }, { "Category": "Style", - "Name": "Theme Creator", - "Description": "Blazor Theme Creator site for MudBlazor library. This app is designed to be used to create and manage themes for MudBlazor, not as part of your application.", - "Link": "https://themes.arctechonline.tech", - "GitHubUserPath": "versile2", - "GitHubRepoPath": "ThemeCreatorMudBlazor" + "Name": "MudX Theme Creator", + "Description": "Blazor Theme Creator site for MudBlazor library. This app is designed to be used to create and export themes for MudBlazor, not as part of your application.", + "Link": "https://themes.mudx.org", + "GitHubUserPath": "MudXtra", + "GitHubRepoPath": "MudXThemeCreator" }, { "Category": "Components", @@ -87,5 +87,13 @@ "Link": "https://phmatray.github.io/FormCraft", "GitHubUserPath": "phmatray", "GitHubRepoPath": "FormCraft" + }, + { + "Category": "Components", + "Name": "MudX Extensions", + "Description": "A collection of components designed for MudBlazor. SecurityCode for seamless verification code input, Outline for scrollspy navigation, CodeBlock for Prism.js highlighting, and more.", + "Link": "https://mudx.org", + "GitHubUserPath": "MudXtra", + "GitHubRepoPath": "MudX" } ] diff --git a/src/MudBlazor.Docs/wwwroot/images/extensions/MudXtra.MudX.webp b/src/MudBlazor.Docs/wwwroot/images/extensions/MudXtra.MudX.webp new file mode 100644 index 0000000000000000000000000000000000000000..8438bb661d42cf7a2b892c43a39a28bcedc9231c GIT binary patch literal 122896 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 literal 0 HcmV?d00001 diff --git a/src/MudBlazor.Docs/wwwroot/images/extensions/versile2.ThemeCreatorMudBlazor.webp b/src/MudBlazor.Docs/wwwroot/images/extensions/MudXtra.MudXThemeCreator.webp similarity index 100% rename from src/MudBlazor.Docs/wwwroot/images/extensions/versile2.ThemeCreatorMudBlazor.webp rename to src/MudBlazor.Docs/wwwroot/images/extensions/MudXtra.MudXThemeCreator.webp From 20a3f70a3023713317871acd8453c925cfa4b27c Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Tue, 22 Jul 2025 20:08:19 -0500 Subject: [PATCH 09/43] MudPopover: Fix MudList Scroll Issue (#11694) --- src/MudBlazor/TScripts/mudPopover.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 2dfd6dbaad7c..264b0d81869e 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -571,6 +571,7 @@ window.mudpopoverHelper = { // set newMaxHeight to be minimum of 3x overflow padding, by default 72px (or 3 items roughly) newMaxHeight = Math.max(newMaxHeight, window.mudpopoverHelper.overflowPadding * 3); popoverContentNode.style.maxHeight = (newMaxHeight) + 'px'; + firstChild.style.maxHeight = (newMaxHeight) + 'px'; popoverContentNode.mudHeight = "setmaxheight"; } } From b24a6cc8510103b38dfd6f725f0b242be40b510d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Wed, 23 Jul 2025 21:21:42 +0200 Subject: [PATCH 10/43] MudTimeSeriesChart: Use InvariantCulture when rendering opacity (#11696) (#11697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Martin Bartoš --- src/MudBlazor/Components/Chart/Charts/TimeSeries.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor index 6ae772b17579..df696d77b014 100644 --- a/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor +++ b/src/MudBlazor/Components/Chart/Charts/TimeSeries.razor @@ -62,12 +62,12 @@ var lineClass = isHovered ? "mud-chart-serie mud-chart-line mud-chart-serie-hovered" : "mud-chart-serie mud-chart-line"; - + if (series.LineDisplayType == LineDisplayType.Area) { var chartArea = _chartAreas[chartLine.Index]; - + } @foreach (var item in _chartDataPoints[chartLine.Index].OrderBy(x => x.Index)) From 86f0de2eb688c0e3d4bccd62c5ca4ad717e96b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VERNOU=20C=C3=A9dric?= <1659796+vernou@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:25:53 +0200 Subject: [PATCH 11/43] MudTable: Maintains group row state when group list is modified (#11681) --- .../Table/TableGroupingNestedTest.razor | 48 +++++++ .../Table/TableGroupingTest3.razor | 40 ++++++ .../Components/TableTests.cs | 122 ++++++++++++++++++ src/MudBlazor/Components/Table/MudTable.razor | 76 ++++++----- .../Components/Table/MudTableGroupRow.razor | 16 +-- 5 files changed, 255 insertions(+), 47 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingNestedTest.razor create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingTest3.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingNestedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingNestedTest.razor new file mode 100644 index 000000000000..c518234e7df3 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingNestedTest.razor @@ -0,0 +1,48 @@ + + + @context.Key + + + @context.Label + + + +@code { + public MudTable? TableInstance { get; private set; } + + public List? Items { get; set; } = [ + new Item { Group = "G1", Nested = "N1", Label = "A" }, + new Item { Group = "G1", Nested = "N1", Label = "B" }, + new Item { Group = "G1", Nested = "N2", Label = "C" }, + new Item { Group = "G2", Nested = "N1", Label = "D" }, + new Item { Group = "G2", Nested = "N1", Label = "E" }, + new Item { Group = "G2", Nested = "N2", Label = "F" }, + new Item { Group = "G3", Nested = "N1", Label = "G" }, + new Item { Group = "G3", Nested = "N1", Label = "H" }, + new Item { Group = "G3", Nested = "N2", Label = "I" } + ]; + + private TableGroupDefinition _groupDefinition = new() { + Indentation = false, + Expandable = true, + IsInitiallyExpanded = false, + Selector = e => e.Group, + InnerGroup = new TableGroupDefinition { + Indentation = false, + Expandable = true, + IsInitiallyExpanded = false, + Selector = e => e.Group + " > " + e.Nested, + } + }; + + public class Item + { + public required string Label { get; set; } + + public required string Group { get; set; } + + public required string Nested { get; set; } + + public override string ToString() => $"({Group} > {Nested}) {Label}"; + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingTest3.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingTest3.razor new file mode 100644 index 000000000000..335e621201be --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableGroupingTest3.razor @@ -0,0 +1,40 @@ + + + @context.Key + + + @context.Label + + + +@code { + public MudTable? TableInstance { get; private set; } + + public List? Items { get; set; } = [ + new Item { Group = "One", Label = "A" }, + new Item { Group = "One", Label = "B" }, + new Item { Group = "One", Label = "C" }, + new Item { Group = "Two", Label = "D" }, + new Item { Group = "Two", Label = "E" }, + new Item { Group = "Two", Label = "F" }, + new Item { Group = "Three", Label = "G" }, + new Item { Group = "Three", Label = "H" }, + new Item { Group = "Three", Label = "I" } + ]; + + private TableGroupDefinition _groupDefinition = new() { + Indentation = false, + Expandable = true, + IsInitiallyExpanded = false, + Selector = e => e.Group + }; + + public class Item + { + public required string Label { get; set; } + + public required string Group { get; set; } + + public override string ToString() => $"({Group}) {Label}"; + } +} diff --git a/src/MudBlazor.UnitTests/Components/TableTests.cs b/src/MudBlazor.UnitTests/Components/TableTests.cs index 4f6caf9229b8..26ffd03c8585 100644 --- a/src/MudBlazor.UnitTests/Components/TableTests.cs +++ b/src/MudBlazor.UnitTests/Components/TableTests.cs @@ -2116,6 +2116,128 @@ public void TableGroupingTest() } + ///

+ /// A table with 3 unexpanded groups. The first group is expanded, next removed. + /// The other groups remain unexpanded. + /// + /// + /// https://github.com/MudBlazor/MudBlazor/issues/10250 + /// + [Test] + public void TableGrouping_ExpandFirstGroupAndRemoveIt_OtherGroupsRemainUnexpanded() + { + // Arrange + + var comp = Context.RenderComponent(); + var table = comp.Instance.TableInstance; + comp.Render(); + + // Assert : Three groups are unexpanded + + table.Context.GroupRows.Count.Should().Be(3); + table.Context.GroupRows.ElementAt(0).Expanded.Should().BeFalse(); + table.Context.GroupRows.ElementAt(1).Expanded.Should().BeFalse(); + table.Context.GroupRows.ElementAt(2).Expanded.Should().BeFalse(); + + // Act : Expend the first group + + comp.FindAll("button")[0].Click(); + + // Assert : Only the first group is expanded + + table.Context.GroupRows.Count.Should().Be(3); + table.Context.GroupRows.ElementAt(0).Expanded.Should().BeTrue(); + table.Context.GroupRows.ElementAt(1).Expanded.Should().BeFalse(); + table.Context.GroupRows.ElementAt(2).Expanded.Should().BeFalse(); + + // Act : Remove the first group + + comp.Instance.Items.RemoveAll(i => i.Group == "One"); + comp.Render(); + + // Assert : Two groups are unexpanded + + table.Context.GroupRows.Count.Should().Be(2); + table.Context.GroupRows.ElementAt(0).Expanded.Should().BeFalse(); + table.Context.GroupRows.ElementAt(1).Expanded.Should().BeFalse(); + } + + /// + /// A table with unexpanded groups and unexpanded nested groups. + /// The first group and its first nested group are expanded. Then remove the first nested group. + /// The other nested group remains unexpanded. + /// + /// + /// https://github.com/MudBlazor/MudBlazor/issues/10250 + /// + [Test] + public void TableGrouping_ExpandFirstNestedGroupAndRemoveIt_OtherNestedGroupsRemainUnexpanded() + { + // Arrange + + var comp = Context.RenderComponent(); + var table = comp.Instance.TableInstance; + comp.Render(); + + // Assert : All groups are unexpanded + + { + var groups = table.Context.GroupRows; + groups.Count.Should().Be(3); + groups.Single(g => g.Items.Key.ToString() == "G1").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G3").Expanded.Should().BeFalse(); + } + + // Act : Expend the first group + + comp.FindAll("button")[0].Click(); + + // Assert : Only the first group is expanded + + { + var groups = table.Context.GroupRows; + groups.Count.Should().Be(5); + groups.Single(g => g.Items.Key.ToString() == "G1").Expanded.Should().BeTrue(); + groups.Single(g => g.Items.Key.ToString() == "G1 > N1").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G1 > N2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G3").Expanded.Should().BeFalse(); + } + + // Act : Expand the first nested group in the first group + + comp.FindAll("button")[1].Click(); + + // Assert : Only the first group and its first nested group are expanded + + { + var groups = table.Context.GroupRows; + groups.Count.Should().Be(5); + groups.Single(g => g.Items.Key.ToString() == "G1").Expanded.Should().BeTrue(); + groups.Single(g => g.Items.Key.ToString() == "G1 > N1").Expanded.Should().BeTrue(); + groups.Single(g => g.Items.Key.ToString() == "G1 > N2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G3").Expanded.Should().BeFalse(); + } + + // Act : Remove the first nested group in first group + + comp.Instance.Items.RemoveAll(i => i.Group == "G1" && i.Nested == "N1"); + comp.Render(); + + // Assert : Only the first group is expanded and its remaining nested group is unexpanded + + { + var groups = table.Context.GroupRows; + groups.Count.Should().Be(4); + groups.Single(g => g.Items.Key.ToString() == "G1").Expanded.Should().BeTrue(); + groups.Single(g => g.Items.Key.ToString() == "G1 > N2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G2").Expanded.Should().BeFalse(); + groups.Single(g => g.Items.Key.ToString() == "G3").Expanded.Should().BeFalse(); + } + } + /// /// Tests the grouping behavior and ensure that it won't break anything else. /// diff --git a/src/MudBlazor/Components/Table/MudTable.razor b/src/MudBlazor/Components/Table/MudTable.razor index 652d05a3a216..4c40902a8eac 100644 --- a/src/MudBlazor/Components/Table/MudTable.razor +++ b/src/MudBlazor/Components/Table/MudTable.razor @@ -50,7 +50,7 @@ { @HeaderContent } - + @if (Columns != null) { @Columns(Def) @@ -82,10 +82,10 @@ { @foreach (var group in GroupItemsPage) { - + } } else @@ -105,31 +105,31 @@ { @if(RowEditingTemplate != null) - { - @RowEditingTemplate(item) - } - @if (Columns != null) - { + { + @RowEditingTemplate(item) + } + @if (Columns != null) + { @Columns(item) - } + } } else { - if (RowTemplate != null) - { - @RowTemplate(item) - } - @if (Columns != null) - { + if (RowTemplate != null) + { + @RowTemplate(item) + } + @if (Columns != null) + { @Columns(item) - } + } } @if (ChildRowContent != null) { @ChildRowContent(item) - } + } @{ rowIndex++; } } @@ -170,7 +170,7 @@ } @if (Columns != null) { - @Columns(Def) + @Columns(Def) } } @@ -194,10 +194,10 @@ { var rowIndex = 0; - RenderFragment rootNode = + RenderFragment rootNode = @ - - + + ; RenderFragment child() => item => @@ -205,32 +205,32 @@ @{ var rowClass = new CssBuilder(RowClass).AddClass(RowClassFunc?.Invoke(item, rowIndex)).AddClass(customClass, !string.IsNullOrEmpty(customClass)).Build(); var rowStyle = new StyleBuilder().AddStyle(RowStyle).AddStyle(RowStyleFunc?.Invoke(item, rowIndex)).Build(); - } + } @if ((!ReadOnly) && Editable && Equals(_editingItem, item)) { @if(RowEditingTemplate != null) - { - @RowEditingTemplate(item) - } - @if (Columns != null) - { + { + @RowEditingTemplate(item) + } + @if (Columns != null) + { @Columns(item) - } + } } else { if (RowTemplate != null) - { - @RowTemplate(item) - } - @if (Columns != null) - { + { + @RowTemplate(item) + } + @if (Columns != null) + { @Columns(item) - } + } } @if (ChildRowContent != null) @@ -238,9 +238,7 @@ @ChildRowContent(item) } @{rowIndex++;} - - ; - + ; return rootNode; } diff --git a/src/MudBlazor/Components/Table/MudTableGroupRow.razor b/src/MudBlazor/Components/Table/MudTableGroupRow.razor index 41d8723caaed..4f5fc0ec53a7 100644 --- a/src/MudBlazor/Components/Table/MudTableGroupRow.razor +++ b/src/MudBlazor/Components/Table/MudTableGroupRow.razor @@ -6,7 +6,7 @@ @*HEADER:*@ @if (HeaderTemplate != null && GroupDefinition != null) { - + @if ((GroupDefinition?.IsThisOrParentExpandable ?? false) || (Context?.Table?.MultiSelection ?? false)) { @@ -38,7 +38,7 @@ } else { -@("There aren't any group definition to use with this component.") + @("There aren't any group definition to use with this component.") } @if (Expanded) @@ -50,10 +50,11 @@ else { @foreach (var group in _innerGroupItems) { - + } } else @@ -74,7 +75,6 @@ else { } - } -} \ No newline at end of file +} From b98825c89f3394faaac8145df3d427d1540fad51 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 25 Jul 2025 18:14:47 -0500 Subject: [PATCH 12/43] Build: Major update to AutoTriage & Consolidate triaging (#11709) --- .github/scripts/AutoTriage.js | 372 ++++++++++++++------- .github/scripts/AutoTriage.prompt | 334 +++++++++++++----- .github/workflows/issue.yml | 4 +- .github/workflows/pr.yml | 4 +- .github/workflows/triage-backlog.yml | 199 ++++++----- .github/workflows/triage-info-required.yml | 28 -- 6 files changed, 607 insertions(+), 334 deletions(-) delete mode 100644 .github/workflows/triage-info-required.yml diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index a471b58e7818..045401591b44 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -1,28 +1,10 @@ /** * AutoTriage - AI-Powered GitHub Issue & PR Analyzer - * + * * Automatically analyzes GitHub issues and pull requests using Gemini, * then applies appropriate labels and helpful comments to improve project management. - * - * Features: - * • Smart labeling based on content analysis - * • Helpful AI-generated comments for issues (not PRs) - * • Safe dry-run mode by default - * • Comprehensive error handling and logging - * - * Usage: - * • Issues: Analyzes, labels, comments, and can close if appropriate - * • Pull Requests: Analyzes and labels only (no comments or closing) - * - * Required Environment Variables: - * • GEMINI_API_KEY - Google Gemini API key - * • GITHUB_TOKEN - GitHub token with repo permissions - * • GITHUB_ISSUE_NUMBER - Issue/PR number to process - * • GITHUB_REPOSITORY - Repository in format "owner/repo" - * • AUTOTRIAGE_ENABLED - Set to 'true' to enable real actions (default: dry-run) - * + * * Original work by Daniel Chalmers © 2025 - * https://gist.github.com/danielchalmers/503d6b9c30e635fccb1221b2671af5f8 */ const fetch = require('node-fetch'); @@ -31,26 +13,25 @@ const core = require('@actions/core'); const fs = require('fs'); const path = require('path'); -// Configuration -const dryRun = process.env.AUTOTRIAGE_ENABLED !== 'true'; -const aiModel = process.env.AUTOTRIAGE_MODEL || 'gemini-2.5-flash'; - -// Load AI prompt template -const promptPath = path.join(__dirname, 'AutoTriage.prompt'); -let basePrompt = ''; -try { - basePrompt = fs.readFileSync(promptPath, 'utf8'); -} catch (err) { - console.error('❌ Failed to load AutoTriage.prompt:', err.message); - process.exit(1); -} +const aiModel = 'gemini-2.5-pro'; +const dbPath = process.env.AUTOTRIAGE_DB_PATH; // Optional path to a JSON file for storing triage history -console.log(`🤖 Using ${aiModel} (${dryRun ? 'DRY RUN' : 'LIVE'})`); +// Allowed actions: 'label', 'comment', 'close', 'edit' +const permissions = new Set( + (process.env.AUTOTRIAGE_PERMISSIONS || '') + .split(',') + .map(p => p.trim()) + .filter(p => p !== '') +); + +function can(action) { + return permissions.has(action) && !permissions.has("none"); +} /** * Call Gemini to analyze the issue content and return structured response */ -async function callGemini(prompt, apiKey) { +async function callGemini(prompt, apiKey, issueNumber) { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${aiModel}:generateContent`, { @@ -66,10 +47,12 @@ async function callGemini(prompt, apiKey) { responseSchema: { type: "object", properties: { - rating: { type: "integer", description: "How much an intervention is needed on a scale of 1 to 10" }, - reason: { type: "string", description: "Brief technical explanation for logging purposes" }, + rating: { type: "integer", description: "How much a human intervention is needed on a scale of 1 to 10" }, + reason: { type: "string", description: "Brief thought process for logging purposes" }, comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, - labels: { type: "array", items: { type: "string" }, description: "Array of labels to apply" } + labels: { type: "array", items: { type: "string" }, description: "Array of labels to apply" }, + close: { type: "boolean", description: "Set to true if the issue should be closed as part of this action", nullable: true }, + newTitle: { type: "string", description: "A new title for the issue or pull request, if needed", nullable: true } }, required: ["rating", "reason", "comment", "labels"] } @@ -87,6 +70,9 @@ async function callGemini(prompt, apiKey) { const data = await response.json(); const analysisResult = data?.candidates?.[0]?.content?.parts?.[0]?.text; + saveArtifact(`${issueNumber}-gemini-full-output.json`, JSON.stringify(data, null, 2)); + saveArtifact(`${issueNumber}-gemini-analysis.json`, analysisResult); + if (!analysisResult) { throw new Error('No analysis result in Gemini response'); } @@ -97,46 +83,114 @@ async function callGemini(prompt, apiKey) { /** * Create metadata string for both logging and AI analysis */ -function formatMetadata(issue) { +async function buildMetadata(issue, owner, repo, octokit) { const isIssue = !issue.pull_request; const itemType = isIssue ? 'issue' : 'pull request'; - const labels = issue.labels?.map(l => typeof l === 'string' ? l : l.name) || []; + const currentLabels = issue.labels?.map(l => (typeof l === 'string' ? l : l.name)) || []; + const hasAssignee = Array.isArray(issue.assignees) ? issue.assignees.length > 0 : !!issue.assignee; + const collaborators = (await octokit.rest.repos.listCollaborators({ owner, repo })).data.map(c => c.login); + + return { + title: issue.title, + state: issue.state, + type: itemType, + number: issue.number, + author: issue.user?.login || 'unknown', + created_at: issue.created_at, + updated_at: issue.updated_at, + comments: issue.comments || 0, + reactions: issue.reactions?.total_count || 0, + labels: currentLabels, + assigned: hasAssignee, + collaborators + }; +} + +/** + * Build a structured JSON report of the issue's full timeline + */ +async function buildTimeline({ octokit, owner, repo, issue_number }) { + // Fetch all events from the issue's timeline + const { data: timelineEvents } = await octokit.rest.issues.listEventsForTimeline({ + owner, + repo, + issue_number, + per_page: 100, // Adjust as needed + }); + + // Map each event to a simplified, standard JSON object + const timelineReport = timelineEvents.map(event => { + const reportEvent = { + event: event.event, + actor: event.actor?.login, + timestamp: event.created_at, + }; + + switch (event.event) { + case 'commented': + return { ...reportEvent, body: event.body }; + + case 'labeled': + return { ...reportEvent, label: { name: event.label.name, color: event.label.color } }; - return `${issue.state} ${itemType} #${issue.number} by ${issue.user?.login || 'unknown'} -Created: ${issue.created_at} -Updated: ${issue.updated_at} -Comments: ${issue.comments || 0}, Reactions: ${issue.reactions?.total_count || 0} -Current labels: ${labels.join(', ') || 'none'}`; + case 'unlabeled': + return { ...reportEvent, label: { name: event.label.name } }; + + case 'renamed': + return { ...reportEvent, title: { from: event.rename.from, to: event.rename.to } }; + + case 'assigned': + case 'unassigned': + return { ...reportEvent, user: event.assignee?.login }; + + case 'closed': + case 'reopened': + case 'locked': + case 'unlocked': + return reportEvent; // These events need no extra properties + + default: + return null; // Ignore other event types (e.g., 'committed', 'reviewed') + } + }).filter(Boolean); // Removes any null entries from the final array + + return timelineReport; } /** * Build the full prompt by combining base template with issue data */ -function buildPrompt(issue, comments) { +async function buildPrompt(issue, owner, repo, octokit, previousContext = null) { + let basePrompt = fs.readFileSync(path.join(__dirname, 'AutoTriage.prompt'), 'utf8'); + const issueText = `${issue.title}\n\n${issue.body || ''}`; - const metadata = formatMetadata(issue); - - // Format comments - let commentsText = 'No comments available.'; - if (comments?.length) { - commentsText = '\nISSUE COMMENTS:'; - comments.forEach((comment, idx) => { - commentsText += `\nComment ${idx + 1} by ${comment.author}:\n${comment.body}`; - }); - } + const metadata = await buildMetadata(issue, owner, repo, octokit); + const timelineReport = await buildTimeline({ octokit, owner, repo, issue_number: issue.number }); + + saveArtifact(`${metadata.number}-github-timeline.md`, JSON.stringify(timelineReport, null, 2)); - return `${basePrompt} + const promptString = `${basePrompt} -ISSUE TO ANALYZE: +=== SECTION: ISSUE TO ANALYZE === ${issueText} -ISSUE METADATA: -${metadata} +=== SECTION: ISSUE METADATA === +${JSON.stringify(metadata, null, 2)} -COMMENTS: -${commentsText} +=== SECTION: ISSUE TIMELINE (JSON) === +${JSON.stringify(timelineReport, null, 2)} -Analyze this issue and provide your structured response.`; +=== SECTION: TRIAGE CONTEXT === +Last triaged: ${previousContext?.lastTriaged} +Previous reasoning: ${previousContext?.previousReasoning} +Current triage date: ${new Date().toISOString()} + +=== SECTION: INSTRUCTIONS === +Analyze this issue, its metadata, and its full timeline. Your entire response must be a single, valid JSON object and nothing else. Do not use Markdown, code fences, or any explanatory text.`; + + // Save prompt to artifacts folder + saveArtifact(`${metadata.number}-gemini-input.md`, promptString); + return promptString; } /** @@ -147,23 +201,18 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { const labelsToAdd = suggestedLabels.filter(l => !currentLabels.includes(l)); const labelsToRemove = currentLabels.filter(l => !suggestedLabels.includes(l)); - // Nothing to change if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { - console.log('🏷️ No label changes suggested'); return; } - // Show what we're changing, prefixing each label with + or - const changes = [ ...labelsToAdd.map(l => `+${l}`), ...labelsToRemove.map(l => `-${l}`) ]; console.log(`🏷️ Label changes: ${changes.join(', ')}`); - // Exit early if dry run - if (dryRun || !octokit) return; + if (!octokit || !can('label')) return; - // Add new labels if (labelsToAdd.length > 0) { await octokit.rest.issues.addLabels({ owner, @@ -173,7 +222,6 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { }); } - // Remove old labels (one by one since GitHub API requires it) for (const label of labelsToRemove) { await octokit.rest.issues.removeLabel({ owner, @@ -188,94 +236,165 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { * Add AI-generated comment to the issue */ async function addComment(issue, comment, owner, repo, octokit) { - // Exit early if dry run - if (dryRun || !octokit) return; + if (!octokit || !can('comment')) return; await octokit.rest.issues.createComment({ owner, repo, issue_number: issue.number, - body: `${comment}\n\n---\n*I'm an AI. Did I miss something? Let me know in a reply!*` + body: comment }); } /** - * Get issue/PR and its comments from GitHub + * Update issue/PR title + */ +async function updateTitle(issue, newTitle, owner, repo, octokit) { + console.log(`✏️ Updating title from "${issue.title}" to "${newTitle}"`); + + if (!octokit || !can('edit')) return; + + await octokit.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + title: newTitle + }); +} + +/** + * Get issue/PR from GitHub */ async function getIssueFromGitHub(owner, repo, number, octokit) { if (!octokit) { throw new Error('GitHub token required to fetch issue data'); } - // Get the issue/PR const { data: issue } = await octokit.rest.issues.get({ owner, repo, issue_number: number }); - // Get comments if there are any - let comments = []; - if (issue.comments > 0) { - const { data: commentsData } = await octokit.rest.issues.listComments({ - owner, - repo, - issue_number: number - }); - comments = commentsData.map(comment => ({ - author: comment.user?.login || 'unknown', - body: comment.body || '' - })); - } + // Comments are now fetched as part of the full timeline in buildPrompt + return issue; +} + +/** + * Close issue with specified reason + */ +async function closeIssue(issue, repo, octokit, reason = 'not_planned') { + console.log(`🔒 Closing #${issue.number} as ${reason}`); + + if (!octokit || !can('close')) return; - return { issue, comments }; + await octokit.rest.issues.update({ + owner: repo.owner, + repo: repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: reason + }); } /** * Main processing function - analyze and act on a single issue/PR */ -async function processIssue(issue, comments, owner, repo, geminiApiKey, octokit) { +async function processIssue(issue, owner, repo, geminiApiKey, octokit, previousContext = null) { const isIssue = !issue.pull_request; - // Skip locked issues if (issue.locked) { console.log(`🔒 Skipping locked ${isIssue ? 'issue' : 'pull request'} #${issue.number}`); return; } - // Log what we're processing - console.log(`📝 ${issue.title}`); - console.log(formatMetadata(issue).replace(/^/gm, '📝 ')); + const metadata = await buildMetadata(issue, owner, repo, octokit); + const formattedMetadata = [ + `#${metadata.number} (${metadata.state} ${metadata.type}) was created by ${metadata.author}`, + `Title: ${metadata.title}`, + `Updated: ${metadata.updated_at}`, + `Labels: ${metadata.labels.join(', ') || 'none'}`, + ].join('\n'); + console.log(formattedMetadata.replace(/^/gm, '📝 ')); - // Build prompt and call AI - const prompt = buildPrompt(issue, comments); + const prompt = await buildPrompt(issue, owner, repo, octokit, previousContext); const start = Date.now(); - const analysis = await callGemini(prompt, geminiApiKey); - console.log(`🤖 Gemini returned analysis in ${Date.now() - start}ms with intervention rating of ${analysis.rating}/10`); + const analysis = await callGemini(prompt, geminiApiKey, metadata.number); - console.log(`🤖 ${analysis.reason}`); + console.log(`🤖 Gemini returned analysis in ${((Date.now() - start) / 1000).toFixed(1)}s with a human intervention rating of ${analysis.rating}/10:`); + console.log(`🤖 "${analysis.reason}"`); - // Apply the AI's suggestions await updateLabels(issue, analysis.labels, owner, repo, octokit); - // Add comment if one was generated if (analysis.comment) { console.log(`💬 Posting comment:`); console.log(analysis.comment.replace(/^/gm, '> ')); - await addComment(issue, analysis.comment, owner, repo, octokit); - } else { - console.log(`💬 No comment suggested.`); + } + + if (analysis.close) { + await closeIssue(issue, { owner, repo }, octokit, 'not_planned'); + } + + if (analysis.newTitle) { + await updateTitle(issue, analysis.newTitle, owner, repo, octokit); } return analysis; } +/** + * Get previous triage context for an issue from the database + */ +function getPreviousContextForIssue(triageDb, issueNumber, issue) { + const triageEntry = triageDb[issueNumber]; + + // 1. Triage if it's never been checked. + if (!triageEntry) { + return { lastTriaged: null, previousReasoning: 'This issue has never been triaged.' }; + } + + // --- Define conditions for re-triaging --- + const MS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000 + const timeSinceTriaged = Date.now() - new Date(triageEntry.lastTriaged).getTime(); + + // 2. Triage if it's been > 14 days since the last check. + const hasExpired = timeSinceTriaged > 14 * MS_PER_DAY; + + // 3. Triage if it's been > 3 days and has a follow-up label. + const labels = (issue.labels || []).map(l => l.name || l); + const needsFollowUp = + (labels.includes('info required') || labels.includes('stale')) && + timeSinceTriaged > 3 * MS_PER_DAY; + + // If any condition for re-triaging is met, return the context. + if (hasExpired || needsFollowUp) { + return { + lastTriaged: triageEntry.lastTriaged, + previousReasoning: triageEntry.previousReasoning || 'No previous reasoning available.', + }; + } + + // Otherwise, no triage is needed. + return null; +} + +/** + * Write contents to an artifact file + */ +function saveArtifact(name, contents) { + const artifactsDir = path.join(process.cwd(), 'artifacts'); + if (!fs.existsSync(artifactsDir)) { + fs.mkdirSync(artifactsDir); + } + const filePath = path.join(artifactsDir, name); + fs.writeFileSync(filePath, contents, 'utf8'); +} + /** * Main entry point */ async function main() { - // Check required environment variables const requiredEnvVars = ['GITHUB_ISSUE_NUMBER', 'GEMINI_API_KEY', 'GITHUB_REPOSITORY']; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { @@ -283,31 +402,56 @@ async function main() { } } - // Parse configuration const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); const issueNumber = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); const geminiApiKey = process.env.GEMINI_API_KEY; - // Setup GitHub API client let octokit = null; if (process.env.GITHUB_TOKEN) { octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + + const rate = await octokit.rest.rateLimit.get(); + if (rate.data.rate.remaining < 1000) { + console.log(`⚠️ GitHub API calls left: ${rate.data.rate.remaining} (resets at ${new Date(rate.data.rate.reset * 1000).toLocaleString()})`); + } else if (rate.data.rate.remaining < 500) { + console.log('❌ Too few GitHub API calls left, ending early to avoid hitting rate limit'); + process.exit(1); + } } else { console.log('⚠️ No GITHUB_TOKEN provided - running in read-only mode'); } - // Get the issue/PR data from GitHub - const { issue, comments } = await getIssueFromGitHub(owner, repo, issueNumber, octokit); + let triageDb = {}; + + if (dbPath && fs.existsSync(dbPath)) { + const contents = fs.readFileSync(dbPath, 'utf8'); + triageDb = contents ? JSON.parse(contents) : {}; + } + + const issue = await getIssueFromGitHub(owner, repo, issueNumber, octokit); - // Process it - await processIssue(issue, comments, owner, repo, geminiApiKey, octokit); + const previousContext = getPreviousContextForIssue(triageDb, issueNumber, issue); - console.log(`\n`); + if (!previousContext) { + console.log(`⏭️ #${issueNumber} does not need to be triaged yet`); + process.exit(2); + } + + console.log("⏭️"); + console.log(`🤖 Using ${aiModel} with [${Array.from(permissions).join(', ') || 'none'}] permissions`); + const analysis = await processIssue(issue, owner, repo, geminiApiKey, octokit, previousContext); + + if (dbPath && analysis && !permissions.has("none")) { + triageDb[issueNumber] = { + lastTriaged: new Date().toISOString(), + previousReasoning: analysis.reason + }; + fs.writeFileSync(dbPath, JSON.stringify(triageDb, null, 2)); + } } -// Run the script main().catch(err => { - console.error('\n❌ Error:', err.message); + console.error('❌ Error:', err.message); core.setFailed(err.message); process.exit(1); }); diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index fa73a1bda16b..d4e74ff8ce42 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -1,50 +1,34 @@ # GitHub Issue Analysis Assistant -## CORE TASK -Analyze the issue and return the structured JSON format. +## CORE TASKS -## VALID LABELS -```json -{ - "accessibility": "Impacts usability for users with disabilities (a11y)", - "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update", - "bug": "An unexpected behavior or defect. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", - "build": "Relates to the project's build process, tooling, CI/CD, README, or repository configuration", - "dependency": "Involves external libraries, packages, or third-party services", - "docs": "Pertains to documentation changes. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", - "enhancement": "A new feature or improvement. Primary issue type. Apply at most one from: 'bug', 'enhancement', 'docs'. If uncertain, apply 'info required'", - "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors", - "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug", - "info required": "Primary action label when an issue is blocked pending necessary details from the author. Must always post a comment explaining what information is needed", - "invalid": "Action label indicating a violation of community standards that warrants closing the issue, or an issue that will be closed due to excessive low quality", - "localization": "Concerns support for multiple languages or regional formats", - "mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android)", - "needs example": "Apply IN ADDITION TO 'info required' when the specific missing information is a code example or a reproduction link", - "needs screenshot": "Apply IN ADDITION TO 'info required' when the specific missing information is a screenshot or video of the visual problem", - "new component": "For tracking an 'enhancement' that proposes or adds a brand-new UI component", - "performance": "Relates to speed, responsiveness, or resource efficiency", - "question": "Action label for a user seeking help and not reporting a bug or requesting a feature. Must always post a comment explaining why the label was added", - "refactor": "For PRs: The primary focus is code reorganization that preserves existing behavior", - "regression": "Apply IN ADDITION TO 'bug' to indicate a high-priority bug where a feature that previously worked is now broken", - "safari": "The issue is specific to the Safari browser on desktop or iOS", - "security": "Impacts application security, including vulnerabilities or data protection", - "stale": "Indicates an issue is inactive and will be closed if no further updates occur", - "tests": "Relates to unit, integration, or other automated testing frameworks" -} -``` +On GitHub, your username is `github-actions` or `github-actions[bot]`. +Analyze the issue and return the structured JSON format. +Explain your reasoning in `reason` in the JSON response. If you wanted to make a change but didn't because of a rule, you can mention that. +All time-based calculations (such as age, stale, or inactivity rules) must be performed by comparing the relevant created or activity date (e.g., issue/PR/comment/label) to the "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. +For testing purposes, don't consider "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. ## PERSONA GUIDELINES -Your role is a first-line triage assistant. Your primary goal is to ensure new issues have enough information for a maintainer to act on them. You are not a discussion moderator, a summarizer, or a participant in ongoing conversations. Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. On GitHub, your username is `github-actions`. -If a user provides a well-reasoned argument or new information that challenges your previous triage decision (such as label assignment, comment, or triage rating), you must evaluate their input in the full context of the issue. If their argument is valid, you should recognize it by acknowledging your mistake, thanking them for the clarification or new details, or updating your decision as appropriate. +Your role is an issue management assistant. Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. +You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. +Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. +If a user provides a well-reasoned argument or new information that challenges your previous triage decision (like label assignment, a comment, or a triage rating), you must evaluate their input in the full context of the issue. If their argument is valid, you should recognize it by acknowledging your mistake, thanking them for the clarification or new details, or updating your decision as appropriate. +Your tone should be kind, helpful, and direct. -Your tone should be kind, helpful, and direct. Avoid overstepping your bounds with statements like "This seems like a useful proposal" or "I will close this issue," which imply a level of authority or ability to perform repository actions. +Don't overstep your bounds with statements which imply a level of self-confidence and authority like: +- "This seems like a useful proposal" +- "it looks like the problem is indeed reproducible" +- "This helps pinpoint the nature of the performance issue" +- "It looks very similar to a bug that was previously fixed in #xxxx" +- "That seems like a sound, low-risk improvement that aligns with best practices" ## PROJECT CONTEXT + - MudBlazor is a Blazor component framework with Material Design - Written in C#, Razor, and CSS with minimal JavaScript - Cross-platform support (Server, WebAssembly, MAUI) -- Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they are still a concern +- Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they're still a concern without needing to update the package - Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com (the generic placeholder link "[https://try.mudblazor.com/snippet](https://try.mudblazor.com/snippet)" counts as a missing reproduction) - Current v8.x.x supports .NET 8 and later - Version migration guides are at [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) @@ -54,7 +38,56 @@ Your tone should be kind, helpful, and direct. Avoid overstepping your bounds wi - Code of Conduct is at [https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md) - Community talk is at [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) +## VALID LABELS + +"accessibility": "Impacts usability for users with disabilities (a11y)" +"breaking change": "For PRs: Signifies that a change will require users to modify their code upon update" +"bug": "An unexpected behavior or defect. Primary issue type." +"build": "Relates to the project's build process, tooling, CI/CD, README, or repository configuration" +"dependency": "Involves external libraries, packages, or third-party services" +"docs": "Pertains to documentation changes. Primary issue type." +"enhancement": "A new feature or improvement. Primary issue type." +"good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors" +"has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug" +"urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" +"info required": "Indicates the issue is blocked pending necessary details from the author for triage" +"invalid": "Action label indicating a violation of community standards that warrants closing the issue, or an issue that will be closed due to excessive low quality" +"localization": "Concerns support for multiple languages or regional formats" +"mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android)" +"needs example": "Specific missing information is a code example or a reproduction link" +"needs screenshot": "Specific missing information is a screenshot or video of the visual problem" +"new component": "For tracking an 'enhancement' that proposes or adds a brand-new UI component" +"performance": "Relates to speed, responsiveness, or resource efficiency" +"question": "Action label for a user seeking help and not reporting a bug or requesting a feature" +"refactor": "For PRs: The primary focus is code reorganization that preserves existing behavior" +"regression": "A high-priority bug where a feature that previously worked is now broken" +"safari": "The issue is specific to the Safari browser on desktop or iOS" +"security": "Impacts application security, including vulnerabilities or data protection" +"stale": "Indicates an issue is inactive and will be closed if no further updates occur" +"tests": "Relates to unit, integration, or other automated testing frameworks" + +## LABELING GUIDELINES + +Only suggest labels from the VALID LABELS list. Never attempt to create new labels. +Only remove labels from the VALID LABELS list. Never remove labels that were added from outside the list. + +Apply at most one from: 'bug', 'enhancement', 'docs'. + +When to apply specific labels: +- 'info required': +    - If you're uncertain of the primary issue type ('bug', 'enhancement', 'docs') initially, apply 'info required'. +    - This label can remain alongside a primary issue type if other crucial details are still missing for full triage after the primary type has been identified. +    - You must always post a comment explaining what information is needed when applying this label. +- 'needs example': Apply this in addition to 'info required' when the specific missing information is a code example or a reproduction link. +- 'needs screenshot': Apply this in addition to 'info required' when the specific missing information is a screenshot or video of the visual problem. +- 'invalid': For extremely low-quality issues that are empty, unintelligible, or contain no actionable information (e.g., a title with a blank body, or a body that just says "doesn't work"). + - In this specific case, apply `invalid` *instead of* `info required`. + - You must always post a comment explaining why the issue was marked invalid. +- 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. You must always post a comment explaining why the label was added and direct to appropriate community channels. +- 'regression': Apply this in addition to 'bug' to indicate a high-priority bug where a feature that previously worked is now broken. + ## COMMON ISSUE TYPES + - Component bugs (MudButton, MudTable, MudDialog, MudForm, etc.) - Styling/theming and CSS customization - Browser compatibility @@ -62,25 +95,25 @@ Your tone should be kind, helpful, and direct. Avoid overstepping your bounds wi - Integration with Blazor Server/WASM/MAUI - Documentation gaps - Caching: Often causes issues after updates. Suggest testing in incognito mode or a private browser window -- Static rendering issues: NOT supported in MudSelect, MudAutocomplete, and some other components. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430) +- Static rendering issues: NOT supported in MudSelect, MudAutocomplete, and most other components. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430) ## QUALITY ASSESSMENT + Consider an issue's age and update frequency. Note engagement like comments and reactions to gauge community interest, especially on older, un-updated issues. **LOW QUALITY indicators (flag for improvement):** - Vague descriptions - Not in English - Visual problems without screenshots -- Bug reports missing reproduction steps or a working example +- Bug reports missing reproduction steps or a working example - Feature requests missing a clear problem description, use case, or motivation (the "why") - Missing expected vs actual behavior - Missing technical details (version, browser, render mode) - Ambiguous or unhelpful issue titles -- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be very certain it is a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question +- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be very certain it's a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question - Code of conduct violations (harassment, trolling, personal attacks) - Extremely low-effort issues (single words, gibberish, spam) - Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work") -- Old bug reports (e.g., over a year old) reported on a significantly outdated version of the library, which have no recent activity (e.g., no comments in the last year) **HIGH QUALITY indicators (ready for labeling):** - Clear component name and specific problem @@ -89,55 +122,190 @@ Consider an issue's age and update frequency. Note engagement like comments and - Technical details and screenshots provided - Descriptive title -## LABELING GUIDELINES -Do not label 'draft' pull requests or closed issues. -Only suggest labels from the valid labels list. Do not attempt to create new labels. - ## COMMENTING GUIDELINES -The decision to comment is based on this question: "Would a human contributor likely ask this question or request this information if they were triaging this issue?". -Only comment if you believe a human maintainer or triager would ask for the same information, and do so before they have to, in order to save them time. The bar for commenting is high: do not comment unless you are confident that most human contributors would do the same in this situation. -If you have previously commented on this issue then the bar to add something new is raised. It should not appear like you are creating spam. -If a previous comment of yours was hidden in the issue by a maintainer, you should be very unsure about the information in that comment and be especially careful not to repeat it or similar information in future responses. -When you see a potential link to a common problem (caching, static rendering, or versioning), briefly mention it as a possibility to explore. -Alert high-priority security vulnerabilities or regressions that could impact many users (ping triage team with `cc @MudBlazor/triage`) -Old or inactive issues can be probed to confirm a bug reported on a very outdated version is still reproducible on the latest version. This has a higher bar for feature requests because they are less likely to expire and can usually be left alone. -Do not assume what is inside the reproduction link because you can't see inside. You only know that it exists. - -**DO NOT comment if:** -- It is a pull request and not an issue -- The issue is closed -- The issue has a `stale` label -- The issue meets all criteria for a high-quality report and needs no further input -- The same information has already been requested by you or someone else recently -- The discussion is an active, ongoing technical debate -- To summarize, thank the user, or compliment the issue + +Your primary goal is to gather information for maintainers efficiently. Only comment when necessary to move an issue forward. + +**Prioritize Commenting For:** +- **Immediate Triage Needs (Human Intervention Rating 9-10):** + - **Always comment on an issue** to flag critical items (e.g., security vulnerabilities, severe regressions). Your comment should include `cc @MudBlazor/triage`. +- **Missing Information for Triage (`info required`):** + - If the `info required` label is added to an issue, you must ALWAYS leave a comment explaining what information is missing and why it is needed for triage. + - If key details are missing in an **issue** (e.g., reproduction, browser, version, screenshots, error messages, clear use case), **you must comment** explaining what's needed. + - **Always add the `info required` label** in this scenario, if not already present. +- **Usage Questions (`question` label):** + - If an **issue** is clearly a help request and not a bug/feature, **you must comment** to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). +- **Stale Issues/PRs (or becoming stale):** +- **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. +- **Common Troubleshooting Suggestions:** + - If a bug report involves a component that is frequently updated, it's possible the issue has already been fixed. **You must comment** to ask the user to verify the problem on the latest version. + - **Components:** `MudDataGrid`, `MudTable`, `MudTextField`, `MudSelect`, `MudAutocomplete`, `MudInput`, `MudDateRangePicker`, `MudPicker`, `MudMenu`, `MudPopover`, `MudDialog`. + - Briefly mention other common potential causes like caching or static rendering issues, framed as suggestions to explore. + +**General Commenting Principles:** +- **Be Direct & Helpful:** Explain *why* information is needed. Frame advice as possibilities. +- **Avoid Over-commenting:** + - If you've previously commented, only do so again if you can help substantially. Avoid repetitive or minor follow-ups. + - Do not comment if the same information has already been requested recently by you or someone else. +- **No PR Suggestions:** Never ask or suggest that someone create a pull request. +- **Neutral Tone:** Never use "we" or imply you represent the MudBlazor team. Do not promise maintainer actions. Use "For anyone investigating this..." +- **Code of Conduct Violations:** Be firm, explain the violation, link to the Code of Conduct, and **immediately close the issue**. + +**DO NOT Comment** +- On a pull request. The only exception is for applying stale rules (marking as stale or closing as stale). This rule is absolute; do not comment on PRs to ask for information, suggest troubleshooting, or for any other reason. +- If the issue is closed. +- If the issue is already high quality and needs no further input from you. +- To join an active, ongoing technical debate. +- To summarize, thank the user, or compliment the issue. +- If the item was created by a repository collaborator. ## COMMENT STYLE -Do not list all the reasons an issue is high quality or good; avoid unnecessary praise or summaries. -Never suggest updating the MudBlazor package version on https://try.mudblazor.com, as it is always running the latest version. + +Don't list all the reasons an issue is high quality or good; avoid unnecessary praise or summaries. Use clean, well-formatted markdown as it will be posted on GitHub. -Write with correct English grammar, punctuation, and spelling. -Each distinct request, question, or statement must be a separate paragraph, separated by a blank line (double newline in markdown). -Do not combine multiple requests or statements into a single run-on sentence or paragraph. -Never use "we" or imply you represent the MudBlazor team or maintainers. Do not promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify the current state. +Always use explicit newline characters (`\n`) within the `comment` field of the JSON response to create line breaks for readability on GitHub. +Never use "we" or imply you represent the MudBlazor team or maintainers. Don't promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify the current state. Explain *why* you need information and frame advice as possibilities based on general web development practices. -Do not attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. +Don't attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. Use "For anyone investigating this..." instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. -For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. - -**Examples:** (do not use verbatim) -- "Could you add a reproduction of the issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug." -- "Could you share the full error message and stack trace from the browser's developer console? That will help pinpoint where the problem is happening." -- "Thanks for the suggestion. Could you elaborate a bit on the use case for this feature? Understanding the problem you're trying to solve helps the team prioritize and design the best solution." -- "This issue was reported against an older version of MudBlazor and has been inactive for some time. Could you please check if this problem still exists in the latest version?" -- "Could you please add a screenshot or video showing the visual issue? It would help for anyone investigating this to see exactly what's happening." -- "I notice you mentioned 'it doesn't work' - could you describe what you expected to happen versus what actually happened? This will help identify the specific problem." -- "Which version of MudBlazor are you using? Also, could you share which browser and .NET version? These details help narrow down potential causes." -- "Could you share the relevant code snippet showing how you're using the component? A minimal example would be really helpful." -- "This looks like it might be a question about usage rather than a bug report. For help with implementation, I'd recommend checking out our [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) where the community can assist you." -- "Hmm, this sounds like it could be related to caching. Have you tried testing in incognito mode to rule that out? Just a thought." -- "This appears to involve static rendering, which isn't supported in MudSelect and some other components. You might want to check the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." +For conduct violations: Be firm, explain the violation, link to the Code of Conduct, and immediately close the issue. +Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" + +**Examples:** +- "Could you provide a reproduction of this issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug. +- "To help pinpoint the problem, could you share the full error message and stack trace from your browser's developer console?" +- "Thanks for the idea. Could you elaborate on the specific use case for this feature? Understanding the problem you're trying to solve helps determine the best approach." +- "Please add a screenshot or video showing the visual issue. This helps anyone investigating clearly see what's happening." +- "You mentioned 'it doesn't work' – could you describe what you expected to happen versus what actually occurred? This will help identify the specific problem." +- "Which version of MudBlazor are you using? Also, please share your browser and .NET versions. These details help narrow down potential causes." +- "Could you provide a minimal code snippet showing how you're using the component? This would be very helpful." +- "This seems like a question about usage rather than a bug report. For implementation help, please check our [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) where the community can assist." +- "This might be related to caching. Have you tried testing in incognito mode to see if that resolves it?" +- "This appears to involve static rendering, which isn't supported in MudSelect and certain other components. You might find more information in the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." -- "This seems like a regression that could affect many users. cc @MudBlazor/triage" -- "Thanks for the report! This issue was reported against MudBlazor 6.x.x. Could you please test if it still occurs with the latest version?" +- "This appears to be a regression that could affect many users. cc @MudBlazor/triage" +- "This was reported against an older MudBlazor version. Could you test if it still occurs with the latest version?" +- "Could you verify if this problem still exists using your provided reproduction link? Our snippet editor at try.mudblazor.com always runs the latest version, so you can just open the link and check." +- "This issue has been marked as invalid because it does not contain a description of the problem. To be actionable, a report needs details explaining the expected behavior versus the actual behavior. Please edit this issue to provide that information." +- "MudDataGrid has received updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" + +Avoid using the examples provided in this prompt verbatim. The examples are for guidance on tone and content. They can be similar but try to rephrase the examples using your own words to fit the specific issue context. + +## ISSUE CLOSING POLICY + +You must never close an issue unless: +- It has been marked as `stale` and meets all the rules for closing stale issues (see below), +- OR it is a code of conduct violation (see above). + +Don't close issues for any other reason, even if they're low quality, invalid, or missing information. Only comment or label in those cases. They will be allowed to go stale and then closed later. + +## STALE ISSUE IDENTIFICATION + +If an issue is marked as `info required` or `stale`, you must re-evaluate the issue after the author's follow-up. +If the issue has been updated, always remove the `stale` label, but the `info required` label may remain if the issue is still missing necessary details. +If the author doesn't provide a satisfactory answer or fails to supply the requested information, you may keep or add the `info required` label and proceed with the stale/close process. + +**Mark an issue as stale if ALL of these conditions are met:** +- The issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` +- The issue does NOT have the `on hold` label +- The issue has no assignee +- The PR was not created by a repository collaborator + +**Mark a pull request as stale if ALL of these conditions are met:** +- The PR has been open for at least 180 days (6 months) +- The PR does NOT have the `on hold` or `breaking change` labels +- The PR has no assignee +- The PR was not created by a repository collaborator + +## STALE ISSUE LABEL + +**When marking an issue as stale:** +1. Add the `stale` label +2. Post this comment: +``` +This issue has been marked as stale. +If you have any updates or additional information, please comment below. + +If no response is received, it will be automatically closed. +``` + +**When marking a PR as stale:** +1. Add the `stale` label +2. Post this comment: +``` +Hi, this pull request hasn't had activity in a few months and has been marked as stale. + +Please let us know if you're still working on it! @MudBlazor/triage +``` + +## CLOSING STALE ISSUES +**Close a stale issue if:** + +- It has the `stale` label +- It has been stale for at least 14 additional days (28 days total since last activity) +- Post this closing comment: +``` +If you're still experiencing this problem, please open a new issue with updated details. +``` + +**Close a stale PR if:** +- It has the `stale` label +- It has been stale for at least 90 additional days (270 days total since last activity) +- Post this closing comment: +``` +This pull request has been closed due to inactivity. + +If you would like to continue working on it, please open a new PR referencing this one. +``` + +## EDITING GUIDELINES + +Suggest a `newTitle` in the JSON **only if** the current title is **very unclear** and the issue is **older than one week with low activity**. + +- Keep changes minimal. +- Never edit titles by repository collaborators. +- Titles should be **sentence-cased**. +- Titles must clearly state the author's request. +- Prioritize clarity and searchability. +- Only edit if it's a significant improvement. + +### Pull Request Title Format + +PR titles must be prefixed with the relevant component. + +**Examples:** +- MudDialog: Fix gaps in body style +- MudColorPicker: Improve ARIA labels & doc page wording + +### Issue Title Format + +Issue titles must NOT use component prefixes (e.g., "MudButton:") or include version numbers. + +**Examples:** +- Make overlay popovers non-modal by default +- Add tooltips to special icon buttons that aren't immediately identifiable +- Add hotkey (Ctrl+K) to open search in docs +- Ripple effect should provide immediate visual feedback + +## HUMAN INTERVENTION GUIDELINES + +The **human intervention rating** indicates how critical it is for a maintainer to address an issue quickly. This rating, on a scale of **1 to 10**, is based on factors such as: + +- **Security Vulnerabilities:** Issues that represent a direct security risk to users or the project. These will typically receive a higher rating (**8-10**). The higher end is for actively exploitable vulnerabilities. +- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component or a large number of users. The severity and impact of the regression will influence the rating (**6-10**). A regression that crashes the app for many users is higher than a minor visual regression. +- **Widespread Impact:** Issues affecting a broad user base or a fundamental part of the framework (**7-9**). This can be a new bug or a critical missing feature. +- **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development (**6-8**). This often means there's no easy workaround. +- **Clarity and Reproducibility:** Well-documented issues with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7** until clarity is achieved). This rewards good reporting. +- **Community Engagement:** High community interest (e.g., many reactions, comments from diverse users) can subtly increase the priority of an issue that isn't inherently critical (e.g., a popular enhancement request might be a **4-6**, whereas a less popular one is a **1-3**). This indicates broader desire. +- **Age of Issue:** Older issues generally have a lower intervention priority. If an issue has gone unaddressed for an extended period without new activity, it typically suggests less urgency or widespread impact, lowering its rating (**1-3** for very old, inactive issues). This prevents the backlog from being constantly re-prioritized by old items. + +Low-quality issues (e.g., empty, unintelligible, or extremely low-effort) do **not** warrant a high human intervention rating (9-10) or a triage ping (`cc @MudBlazor/triage`). These issues will be handled by the stale bot. + +### Intervention Rating Scale + +- **1-3 (Low Intervention):** Routine enhancements, minor documentation fixes, or very old, inactive issues with low impact. These can be triaged at leisure. *Examples: Cosmetic bug in an obscure component, request for a minor new prop, a year-old issue with no comments.* +- **4-6 (Moderate Intervention):** General bug reports with clear steps but not critical impact, clear enhancement requests with moderate impact, or issues requiring some investigation but not immediate action. *Examples: Component behaving slightly unexpectedly but with a workaround, a well-defined feature request for a commonly used component.* +- **7-8 (High Intervention):** Important bugs impacting several users, significant enhancements, or issues that, while not critical, should be addressed within a reasonable timeframe. *Examples: A bug that makes a widely used component difficult to use without a clear workaround, a feature that significantly improves developer experience or solves a common integration challenge.* +- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or issues causing severe application failures. If an issue warrants this rating, you should comment with "cc @MudBlazor/triage" to alert the triage team immediately. + - Apply the `urgent` label for issues or PRs with this rating. diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 3da4db93f079..08ce9349080c 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -18,11 +18,11 @@ jobs: - name: Install dependencies run: npm install node-fetch@2 @actions/core @octokit/rest - - name: Run AI triage script + - name: Triage with AI Assistant env: GITHUB_ISSUE_NUMBER: ${{ github.event.issue.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_ENABLED: ${{ vars.AUTOTRIAGE_ENABLED }} + AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c9cc71e7eb81..ace901158162 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,11 +18,11 @@ jobs: - name: Install dependencies run: npm install node-fetch@2 @actions/core @octokit/rest - - name: Run AI triage script + - name: Triage with AI Assistant env: GITHUB_ISSUE_NUMBER: ${{ github.event.pull_request.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_ENABLED: ${{ vars.AUTOTRIAGE_ENABLED }} + AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 76e97954ef06..44606e4c6d93 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -2,7 +2,7 @@ name: Triage Backlog on: schedule: - - cron: '0 6 * * *' # Gemini rates reset at 7am, so we can use up the remaining quota for the day. + - cron: '0 6 * * *' workflow_dispatch: inputs: backlog-size: @@ -11,123 +11,112 @@ on: default: '5' type: string issue-numbers: - description: 'Or use issue numbers (comma-separated)' + description: 'Or use issue numbers (space-separated)' required: false type: string - model: - description: 'Gemini model' + permissions: + description: 'Permissions (`none` for dry run)' required: false - default: 'gemini-2.5-flash' + default: 'label, comment, close, edit' type: string - dry-run: - description: 'Dry-run (no actual changes)' - required: false - default: true - type: boolean - -env: - DRY_RUN: ${{ inputs.dry-run || vars.AUTOTRIAGE_ENABLED != 'true' }} jobs: - stale-issues: + auto-triage: runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || github.repository == 'MudBlazor/MudBlazor' - + if: github.event_name == 'workflow_dispatch' || vars.AUTOTRIAGE_PERMISSIONS # Run manually or on a schedule if permissions are set up. steps: - - name: Mark stale issues - uses: actions/stale@v9 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 with: - stale-issue-label: 'stale' - any-of-issue-labels: 'info required,question,answered,not planned,duplicate,invalid,fixed' - exempt-issue-labels: 'on hold' - days-before-issue-stale: 14 - days-before-issue-close: 14 - stale-issue-message: | - This issue has been marked as stale. - If you have any updates or additional information, please comment below. + node-version: '22' - If no response is received, it will be automatically closed. - close-issue-message: | - If you're still experiencing this problem, please open a new issue with updated details. + - name: Install dependencies + run: npm install node-fetch@2 @actions/core @octokit/rest - stale-pr-label: 'stale' - exempt-pr-labels: 'on hold,breaking change' - days-before-pr-stale: 180 - days-before-pr-close: 90 - stale-pr-message: | - Hi, this pull request hasn't had activity in a few months and has been marked as stale. + - name: Cache triage database + uses: actions/cache@v4 + id: cache-db + with: + path: triage-db.json + # 1. The key is unique on every run, forcing a save on success. + key: ${{ runner.os }}-triage-database-${{ github.run_id }} + # 2. Restore-keys finds the most recent cache to use at the start. + restore-keys: | + ${{ runner.os }}-triage-database- - Please let us know if you're still working on it! @MudBlazor/triage - close-pr-message: | - This pull request has been closed due to inactivity. + - name: Triage Issues + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTOTRIAGE_PERMISSIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.permissions || vars.AUTOTRIAGE_PERMISSIONS }} + AUTOTRIAGE_DB_PATH: ${{ github.workspace }}/triage-db.json + MAX_ISSUES: ${{ github.event_name == 'schedule' && 50 || inputs.backlog-size }} + run: | + if [ -n "${{ inputs.issue-numbers }}" ]; then + echo "Processing specified issues: ${{ inputs.issue-numbers }}" + issue_numbers="${{ inputs.issue-numbers }}" + max_count="" + else + echo "Processing up to $MAX_ISSUES issues from the backlog" + issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc -label:enhancement -label:"on hold"' --json number --jq '.[].number') + max_count="$MAX_ISSUES" + fi - If you would like to continue working on it, please open a new PR referencing this one. + count=0 + for issue_number in $issue_numbers; do + if [ -n "$max_count" ] && [ "$count" -ge "$max_count" ]; then + break + fi + + export GITHUB_ISSUE_NUMBER="$issue_number" - exempt-all-assignees: true - debug-only: ${{ env.DRY_RUN }} + # This structure tells the shell we are handling the exit code ourselves + if node .github/scripts/AutoTriage.js; then + # SUCCESS (exit code 0) + count=$((count + 1)) + if [ -n "$max_count" ]; then + left=$((max_count - count)) + echo "⏭️ $(date '+%Y-%m-%d %H:%M:%S'): $left left" + fi + sleep 10 # Rate limit + else + # FAILURE (exit code is non-zero) + exit_code=$? + if [ "$exit_code" -eq 1 ]; then + # Fatal error, fail the entire workflow + echo "❌ Fatal error while processing #${issue_number}" + exit 1 + elif [ "$exit_code" -eq 2 ]; then + # Skippable issue. Do nothing and let the loop continue. + # The step will not be marked as failed. + : + else + # Any other unexpected error + echo "❌ Unexpected error (exit code $exit_code) for #${issue_number}" + exit "$exit_code" + fi + fi + done - auto-triage: - runs-on: ubuntu-latest - needs: stale-issues - if: github.event_name == 'workflow_dispatch' || github.repository == 'MudBlazor/MudBlazor' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Install dependencies - run: npm install node-fetch@2 @actions/core @octokit/rest - - - name: Get issues to process - id: get-issues - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if [ -n "${{ inputs.issue-numbers }}" ]; then - # Process specific issues - echo "Processing specific issues: ${{ inputs.issue-numbers }}" - echo "${{ inputs.issue-numbers }}" | tr ', ' '\n' | grep -v '^$' | jq -R . | jq -s . > issues.json - else - # Find unlabeled issues for automated processing - backlog_size="${{ inputs.backlog-size || '30' }}" - echo "Finding $backlog_size unlabeled issues" - gh issue list \ - --state open \ - --limit 99999 \ - --json number,labels \ - --jq '.[] | select((.labels // [] | length == 0)) | .number' \ - | head -n "$backlog_size" \ - | jq -R . | jq -s . > issues.json - fi - - issue_count=$(cat issues.json | jq length) - echo "Found $issue_count issues to process" - echo "issue-count=$issue_count" >> $GITHUB_OUTPUT - - if [ "$issue_count" -eq 0 ]; then - echo "No issues found to process" - exit 0 - fi - - - name: Process issues - if: steps.get-issues.outputs.issue-count > 0 - env: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_ENABLED: ${{ env.DRY_RUN == 'false' }} - AUTOTRIAGE_MODEL: ${{ inputs.model }} - run: | - cat issues.json | jq -r '.[]' | while read -r issue_number; do - if [ -n "$issue_number" ]; then - echo "——————————————————————————————————Issue #$issue_number——————————————————————————————————" - export GITHUB_ISSUE_NUMBER="$issue_number" - node .github/scripts/AutoTriage.js - # Rate limiting delay - sleep 3 + - name: Display database stats + if: always() + run: | + if [ -f "triage-db.json" ]; then + total_entries=$(jq 'length' triage-db.json) + echo "Database now contains $total_entries triaged issues" + echo "Most recent entries:" + jq -r 'to_entries | sort_by(.value.lastTriaged) | reverse | .[0:5] | .[] | " #\(.key): \(.value.lastTriaged)"' triage-db.json || echo " No entries found" + else + echo "Database file not found" fi - done + + - name: Upload triage artifacts + uses: actions/upload-artifact@v4 + with: + name: triage-artifacts + path: | + triage-db.json + artifacts/ diff --git a/.github/workflows/triage-info-required.yml b/.github/workflows/triage-info-required.yml deleted file mode 100644 index 67f81ef9b5aa..000000000000 --- a/.github/workflows/triage-info-required.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Triage 'info required' - -on: - issue_comment: - types: [created] - -jobs: - remove_label_on_author_comment: - runs-on: ubuntu-latest - - # This 'if' condition ensures the job only runs when all criteria are met: - # 1. The comment author is the same as the issue author. - # 2. The issue currently has the label "info required". - if: | - github.event.comment.user.login == github.event.issue.user.login && - contains(github.event.issue.labels.*.name, 'info required') - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Remove "info required" label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: | - echo "Removing 'info required' label from issue #$ISSUE_NUMBER" - gh issue edit "$ISSUE_NUMBER" --remove-label "info required" --remove-label "stale" From 85b50689ec315fcad7c4c7c9a080a3ee202df70b Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 25 Jul 2025 18:27:45 -0500 Subject: [PATCH 13/43] Build: Add Gemini review rules (#11682) --- .gemini/config.yaml | 12 ++ .gemini/styleguide.md | 259 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 .gemini/config.yaml create mode 100644 .gemini/styleguide.md diff --git a/.gemini/config.yaml b/.gemini/config.yaml new file mode 100644 index 000000000000..9b4e9715cc9f --- /dev/null +++ b/.gemini/config.yaml @@ -0,0 +1,12 @@ +# Config for the Gemini Pull Request Review Bot. +# https://github.com/marketplace/gemini-code-assist +have_fun: false +code_review: + disable: false + comment_severity_threshold: MEDIUM + max_review_comments: -1 + pull_request_opened: + help: false + summary: true + code_review: true +ignore_patterns: [] diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md new file mode 100644 index 000000000000..19d8a837ac1e --- /dev/null +++ b/.gemini/styleguide.md @@ -0,0 +1,259 @@ +# MudBlazor Style Guide for Gemini Code Assist + +This style guide is optimized for AI-assisted code review of MudBlazor pull requests. It provides clear, actionable rules with examples for automated detection of common issues. + +## 🔒 Critical Rules (Block PR if Violated) + +### 1. Parameter State Framework Violations + +**BLOCK PR**: Parameters with logic in setters +```csharp +// ❌ FORBIDDEN - Will break component lifecycle +[Parameter] +public bool Expanded +{ + get => _expanded; + set + { + _expanded = value; + StateHasChanged(); // Logic in setter - BLOCK PR + } +} +``` + +**REQUIRE**: Use ParameterState framework +```csharp +// ✅ CORRECT - Required pattern +private readonly ParameterState _expandedState; + +[Parameter] +public bool Expanded { get; set; } + +public MyComponent() +{ + using var registerScope = CreateRegisterScope(); + _expandedState = registerScope.RegisterParameter(nameof(Expanded)) + .WithParameter(() => Expanded) + .WithEventCallback(() => ExpandedChanged) + .WithChangeHandler(OnExpandedChangedAsync); +} +``` + +### 2. Direct Parameter Assignment + +**BLOCK PR**: Direct parameter property assignment +```csharp +// ❌ FORBIDDEN +private void Toggle() +{ + Expanded = !Expanded; // Direct assignment - BLOCK PR +} +``` + +**REQUIRE**: Use ParameterState.SetValueAsync +```csharp +// ✅ CORRECT +private Task ToggleAsync() +{ + return _expandedState.SetValueAsync(!_expandedState.Value); +} +``` + +### 3. Imperative Parameter Setting on Other Components + +**BLOCK PR**: Setting parameters via component references +```csharp +// ❌ FORBIDDEN - Causes BL0005 warning +private void Update() +{ + _componentRef.MyParameter = newValue; // BLOCK PR +} +``` + +### 4. Missing Unit Tests for Logic + +**BLOCK PR**: New C# logic without corresponding bUnit tests +- Any new method with conditional logic +- Any bug fix without regression test +- Any parameter change handler without test coverage + +## ⚠️ High Priority Issues (Flag for Review) + +### API Breaking Changes + +**FLAG**: Changes to public component APIs +- Adding required parameters +- Removing or renaming public properties/methods +- Changing parameter types +- Modifying default parameter values +- Changes to EventCallback signatures + +**REQUIRE**: Explicit documentation of breaking changes in PR description + +### Formatting and Style Violations + +**FLAG**: Incorrect indentation or formatting +```csharp +// ❌ Wrong indentation (should be 4 spaces for C#) +public class MyComponent : ComponentBase +{ + private string _field; // 2 spaces - FLAG + +// ❌ Wrong brace placement +public void Method() { // Same line - FLAG +} +``` + +```csharp +// ✅ Correct formatting +public class MyComponent : ComponentBase +{ + private string _field; // 4 spaces + + public void Method() + { // New line + } +} +``` + +### Performance Anti-Patterns + +**FLAG**: Performance issues +```csharp +// ❌ Synchronous operations in async context +public async Task LoadDataAsync() +{ + var result = SomeAsyncMethod().Result; // FLAG - blocking async +} + +// ❌ Missing virtualization for large lists + + @foreach (var item in thousandsOfItems) // FLAG if >100 items + { + @item.Name + } + +``` + +## 📋 Code Review Checklist + +### Component Structure +- [ ] Uses `CssBuilder` for dynamic CSS classes +- [ ] All public properties have summary comments +- [ ] Private fields use `_camelCase` naming +- [ ] Public members use `PascalCase` naming +- [ ] Files end with newline + +### Parameter Handling +- [ ] No logic in parameter setters +- [ ] All parameters use ParameterState framework +- [ ] EventCallbacks registered with `WithEventCallback()` +- [ ] Parameter updates use `SetValueAsync()` method + +### Testing Requirements +- [ ] bUnit test exists for new components +- [ ] Test coverage for all conditional logic paths +- [ ] Tests use `InvokeAsync` for parameter changes +- [ ] Tests re-query DOM elements (don't store `Find()` results) +- [ ] Regression tests for bug fixes + +### Accessibility +- [ ] Semantic HTML elements used (`button`, `input`, etc.) +- [ ] ARIA attributes for custom components +- [ ] Keyboard navigation support +- [ ] Focus management implemented +- [ ] Color contrast meets WCAG AA standards + +### Documentation +- [ ] New components have documentation pages +- [ ] Examples ordered simple to complex +- [ ] Visual changes include screenshots/GIFs +- [ ] Breaking changes documented in PR description + +## 🎯 Specific Patterns to Detect + +### Anti-Pattern Detection + +Look for these problematic patterns in PRs: + +```csharp +// Parameter setter with logic +set { _field = value; DoSomething(); } // BLOCK + +// Direct parameter assignment +SomeParameter = newValue; // BLOCK + +// Component reference parameter setting +@ref="comp" ... comp.Parameter = value; // BLOCK + +// Storing Find results +var button = comp.Find("button"); // FLAG +// Later... button.Click(); // May be stale + +// Missing async/await +public async Task Method() +{ + DoSomethingAsync().Wait(); // FLAG +} + +// Hard-coded styles instead of CSS variables +style="color: #1976d2;" // FLAG - use CSS variable + +// Missing virtualization indicators +@foreach (var item in Items) // FLAG if Items.Count > 100 +``` + +### Required Patterns + +Ensure these patterns are present: + +```csharp +// ParameterState registration in constructor +public ComponentName() +{ + using var registerScope = CreateRegisterScope(); + // Parameter registrations here +} + +// Change handlers for parameters +private async Task OnParameterChangedAsync() +{ + // Logic here, not in setter +} + +// Proper async usage +public async Task MethodAsync() +{ + await SomeAsyncOperation(); +} + +// bUnit test structure +[Test] +public void ComponentTest() +{ + var comp = ctx.RenderComponent(); + // Test logic with proper async handling +} +``` + +## 📚 Quick Reference + +### File Types and Indentation +- `.cs`, `.razor`: 4 spaces +- `.json`, `.csproj`, `.scss`: 2 spaces + +### Naming Conventions +- Private fields: `_camelCase` +- Parameters/locals: `camelCase` +- Public members: `PascalCase` + +### Required Imports Usage +- Dynamic CSS: `CssBuilder` +- Parameter handling: `ParameterState` +- Testing: `bUnit` with `InvokeAsync` + +### PR Requirements +- Title: `: (#issue)` +- Target: `dev` branch +- All CI checks passing +- Documentation for public APIs From ce56dd855b99b008b0516a467a8849ed5713d4ba Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 25 Jul 2025 22:23:28 -0500 Subject: [PATCH 14/43] Build: Refactor and fine-tune AutoTriage (#11711) --- .github/scripts/AutoTriage.js | 395 +++++++++------------------ .github/scripts/AutoTriage.prompt | 3 +- .github/workflows/triage-backlog.yml | 45 ++- 3 files changed, 155 insertions(+), 288 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index 045401591b44..a73920869f08 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -13,87 +13,83 @@ const core = require('@actions/core'); const fs = require('fs'); const path = require('path'); -const aiModel = 'gemini-2.5-pro'; -const dbPath = process.env.AUTOTRIAGE_DB_PATH; // Optional path to a JSON file for storing triage history - -// Allowed actions: 'label', 'comment', 'close', 'edit' -const permissions = new Set( +// Global variables +const AI_MODEL = 'gemini-2.5-pro'; +const DB_PATH = process.env.AUTOTRIAGE_DB_PATH; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; +const GITHUB_ISSUE_NUMBER = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); +const [OWNER, REPO] = (GITHUB_REPOSITORY || '').split('/'); +const issueParams = { owner: OWNER, repo: REPO, issue_number: GITHUB_ISSUE_NUMBER }; + +// Allowed actions: 'label', 'comment', 'close', 'edit'; 'none' disables all actions. +let PERMISSIONS = new Set( (process.env.AUTOTRIAGE_PERMISSIONS || '') .split(',') .map(p => p.trim()) .filter(p => p !== '') ); +if (PERMISSIONS.has('none')) PERMISSIONS.clear(); + +const can = action => PERMISSIONS.has(action); + +// Call Gemini to analyze the issue content and return structured response +async function callGemini(prompt) { + const payload = { + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { + responseMimeType: "application/json", + responseSchema: { + type: "object", + properties: { + rating: { type: "integer", description: "How much a human intervention is needed on a scale of 1 to 10" }, + reason: { type: "string", description: "Brief thought process for logging purposes" }, + comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, + labels: { type: "array", items: { type: "string" }, description: "The final set of labels the issue should have" }, + close: { type: "boolean", description: "Set to true if the issue should be closed as part of this action", nullable: true }, + newTitle: { type: "string", description: "A new title for the issue or pull request", nullable: true } + }, + required: ["rating", "reason", "comment", "labels"] + } + } + }; -function can(action) { - return permissions.has(action) && !permissions.has("none"); -} - -/** - * Call Gemini to analyze the issue content and return structured response - */ -async function callGemini(prompt, apiKey, issueNumber) { const response = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models/${aiModel}:generateContent`, + `https://generativelanguage.googleapis.com/v1beta/models/${AI_MODEL}:generateContent`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-goog-api-key': apiKey - }, - body: JSON.stringify({ - contents: [{ parts: [{ text: prompt }] }], - generationConfig: { - responseMimeType: "application/json", - responseSchema: { - type: "object", - properties: { - rating: { type: "integer", description: "How much a human intervention is needed on a scale of 1 to 10" }, - reason: { type: "string", description: "Brief thought process for logging purposes" }, - comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, - labels: { type: "array", items: { type: "string" }, description: "Array of labels to apply" }, - close: { type: "boolean", description: "Set to true if the issue should be closed as part of this action", nullable: true }, - newTitle: { type: "string", description: "A new title for the issue or pull request, if needed", nullable: true } - }, - required: ["rating", "reason", "comment", "labels"] - } - } - }), + headers: { 'Content-Type': 'application/json', 'X-goog-api-key': GEMINI_API_KEY }, + body: JSON.stringify(payload), timeout: 60000 } ); if (!response.ok) { - const errText = await response.text(); - throw new Error(`Gemini API error: ${response.status} ${response.statusText} — ${errText}`); + throw new Error(`Gemini API error: ${response.status} ${response.statusText} — ${await response.text()}`); } const data = await response.json(); - const analysisResult = data?.candidates?.[0]?.content?.parts?.[0]?.text; + const result = data?.candidates?.[0]?.content?.parts?.[0]?.text; - saveArtifact(`${issueNumber}-gemini-full-output.json`, JSON.stringify(data, null, 2)); - saveArtifact(`${issueNumber}-gemini-analysis.json`, analysisResult); + saveArtifact('gemini-output.json', JSON.stringify(data, null, 2)); + saveArtifact('gemini-analysis.json', result); - if (!analysisResult) { - throw new Error('No analysis result in Gemini response'); - } - - return JSON.parse(analysisResult); + if (!result) throw new Error('No analysis result in Gemini response'); + return JSON.parse(result); } -/** - * Create metadata string for both logging and AI analysis - */ -async function buildMetadata(issue, owner, repo, octokit) { +// Create issue metadata for analysis +async function buildMetadata(issue, octokit) { const isIssue = !issue.pull_request; - const itemType = isIssue ? 'issue' : 'pull request'; - const currentLabels = issue.labels?.map(l => (typeof l === 'string' ? l : l.name)) || []; + const currentLabels = issue.labels?.map(l => l.name || l) || []; const hasAssignee = Array.isArray(issue.assignees) ? issue.assignees.length > 0 : !!issue.assignee; - const collaborators = (await octokit.rest.repos.listCollaborators({ owner, repo })).data.map(c => c.login); + const { data: collaboratorsData } = await octokit.rest.repos.listCollaborators({ owner: OWNER, repo: REPO }); return { title: issue.title, state: issue.state, - type: itemType, + type: isIssue ? 'issue' : 'pull request', number: issue.number, author: issue.user?.login || 'unknown', created_at: issue.created_at, @@ -102,79 +98,50 @@ async function buildMetadata(issue, owner, repo, octokit) { reactions: issue.reactions?.total_count || 0, labels: currentLabels, assigned: hasAssignee, - collaborators + collaborators: collaboratorsData.map(c => c.login) }; } -/** - * Build a structured JSON report of the issue's full timeline - */ -async function buildTimeline({ octokit, owner, repo, issue_number }) { - // Fetch all events from the issue's timeline +// Build timeline report from GitHub events +async function buildTimeline(octokit, issue_number) { const { data: timelineEvents } = await octokit.rest.issues.listEventsForTimeline({ - owner, - repo, + owner: OWNER, + repo: REPO, issue_number, - per_page: 100, // Adjust as needed + per_page: 100 }); - // Map each event to a simplified, standard JSON object - const timelineReport = timelineEvents.map(event => { - const reportEvent = { - event: event.event, - actor: event.actor?.login, - timestamp: event.created_at, - }; - + return timelineEvents.map(event => { + const base = { event: event.event, actor: event.actor?.login, timestamp: event.created_at }; switch (event.event) { - case 'commented': - return { ...reportEvent, body: event.body }; - - case 'labeled': - return { ...reportEvent, label: { name: event.label.name, color: event.label.color } }; - - case 'unlabeled': - return { ...reportEvent, label: { name: event.label.name } }; - - case 'renamed': - return { ...reportEvent, title: { from: event.rename.from, to: event.rename.to } }; - + case 'commented': return { ...base, body: event.body }; + case 'labeled': return { ...base, label: { name: event.label.name, color: event.label.color } }; + case 'unlabeled': return { ...base, label: { name: event.label.name } }; + case 'renamed': return { ...base, title: { from: event.rename.from, to: event.rename.to } }; case 'assigned': - case 'unassigned': - return { ...reportEvent, user: event.assignee?.login }; - + case 'unassigned': return { ...base, user: event.assignee?.login }; case 'closed': case 'reopened': case 'locked': - case 'unlocked': - return reportEvent; // These events need no extra properties - - default: - return null; // Ignore other event types (e.g., 'committed', 'reviewed') + case 'unlocked': return base; + default: return null; } - }).filter(Boolean); // Removes any null entries from the final array - - return timelineReport; + }).filter(Boolean); } -/** - * Build the full prompt by combining base template with issue data - */ -async function buildPrompt(issue, owner, repo, octokit, previousContext = null) { +// Build the full prompt by combining base template with issue data +async function buildPrompt(issue, octokit, previousContext = null) { let basePrompt = fs.readFileSync(path.join(__dirname, 'AutoTriage.prompt'), 'utf8'); const issueText = `${issue.title}\n\n${issue.body || ''}`; - const metadata = await buildMetadata(issue, owner, repo, octokit); - const timelineReport = await buildTimeline({ octokit, owner, repo, issue_number: issue.number }); - - saveArtifact(`${metadata.number}-github-timeline.md`, JSON.stringify(timelineReport, null, 2)); - + const metadata = await buildMetadata(issue, octokit); + const timelineReport = await buildTimeline(octokit, issue.number); const promptString = `${basePrompt} === SECTION: ISSUE TO ANALYZE === ${issueText} -=== SECTION: ISSUE METADATA === +=== SECTION: ISSUE METADATA (JSON) === ${JSON.stringify(metadata, null, 2)} === SECTION: ISSUE TIMELINE (JSON) === @@ -188,22 +155,19 @@ Current triage date: ${new Date().toISOString()} === SECTION: INSTRUCTIONS === Analyze this issue, its metadata, and its full timeline. Your entire response must be a single, valid JSON object and nothing else. Do not use Markdown, code fences, or any explanatory text.`; - // Save prompt to artifacts folder - saveArtifact(`${metadata.number}-gemini-input.md`, promptString); + saveArtifact(`github-timeline.md`, JSON.stringify(timelineReport, null, 2)); + saveArtifact(`gemini-input.md`, promptString); return promptString; } -/** - * Update GitHub issue labels based on AI recommendations - */ -async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { - const currentLabels = issue.labels?.map(l => typeof l === 'string' ? l : l.name) || []; +// Update GitHub issue labels +async function updateLabels(suggestedLabels, octokit) { + const { data: issue } = await octokit.rest.issues.get(issueParams); + const currentLabels = issue.labels?.map(l => l.name || l) || []; const labelsToAdd = suggestedLabels.filter(l => !currentLabels.includes(l)); const labelsToRemove = currentLabels.filter(l => !suggestedLabels.includes(l)); - if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { - return; - } + if (labelsToAdd.length === 0 && labelsToRemove.length === 0) return; const changes = [ ...labelsToAdd.map(l => `+${l}`), @@ -214,140 +178,75 @@ async function updateLabels(issue, suggestedLabels, owner, repo, octokit) { if (!octokit || !can('label')) return; if (labelsToAdd.length > 0) { - await octokit.rest.issues.addLabels({ - owner, - repo, - issue_number: issue.number, - labels: labelsToAdd - }); + await octokit.rest.issues.addLabels({ ...issueParams, labels: labelsToAdd }); } for (const label of labelsToRemove) { - await octokit.rest.issues.removeLabel({ - owner, - repo, - issue_number: issue.number, - name: label - }); + await octokit.rest.issues.removeLabel({ ...issueParams, name: label }); } } -/** - * Add AI-generated comment to the issue - */ -async function addComment(issue, comment, owner, repo, octokit) { +// Add AI-generated comment to the issue +async function createComment(body, octokit) { if (!octokit || !can('comment')) return; - - await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: issue.number, - body: comment - }); + await octokit.rest.issues.createComment({ ...issueParams, body: body }); } -/** - * Update issue/PR title - */ -async function updateTitle(issue, newTitle, owner, repo, octokit) { - console.log(`✏️ Updating title from "${issue.title}" to "${newTitle}"`); - +// Update issue/PR title +async function updateTitle(title, newTitle, octokit) { + console.log(`✏️ Updating title from "${title}" to "${newTitle}"`); if (!octokit || !can('edit')) return; - - await octokit.rest.issues.update({ - owner, - repo, - issue_number: issue.number, - title: newTitle - }); -} - -/** - * Get issue/PR from GitHub - */ -async function getIssueFromGitHub(owner, repo, number, octokit) { - if (!octokit) { - throw new Error('GitHub token required to fetch issue data'); - } - - const { data: issue } = await octokit.rest.issues.get({ - owner, - repo, - issue_number: number - }); - - // Comments are now fetched as part of the full timeline in buildPrompt - return issue; + await octokit.rest.issues.update({ ...issueParams, title: newTitle }); } -/** - * Close issue with specified reason - */ -async function closeIssue(issue, repo, octokit, reason = 'not_planned') { - console.log(`🔒 Closing #${issue.number} as ${reason}`); - +// Close issue with specified reason +async function closeIssue(octokit, reason = 'not_planned') { + console.log(`🔒 Closing issue as ${reason}`); if (!octokit || !can('close')) return; - - await octokit.rest.issues.update({ - owner: repo.owner, - repo: repo.repo, - issue_number: issue.number, - state: 'closed', - state_reason: reason - }); + await octokit.rest.issues.update({ ...issueParams, state: 'closed', state_reason: reason }); } -/** - * Main processing function - analyze and act on a single issue/PR - */ -async function processIssue(issue, owner, repo, geminiApiKey, octokit, previousContext = null) { - const isIssue = !issue.pull_request; - - if (issue.locked) { - console.log(`🔒 Skipping locked ${isIssue ? 'issue' : 'pull request'} #${issue.number}`); - return; - } - - const metadata = await buildMetadata(issue, owner, repo, octokit); +// Main processing function - analyze and act on a single issue/PR +async function processIssue(issue, octokit, previousContext = null) { + const metadata = await buildMetadata(issue, octokit); const formattedMetadata = [ `#${metadata.number} (${metadata.state} ${metadata.type}) was created by ${metadata.author}`, `Title: ${metadata.title}`, `Updated: ${metadata.updated_at}`, `Labels: ${metadata.labels.join(', ') || 'none'}`, - ].join('\n'); - console.log(formattedMetadata.replace(/^/gm, '📝 ')); + ].map(line => `📝 ${line}`).join('\n'); + console.log(formattedMetadata); - const prompt = await buildPrompt(issue, owner, repo, octokit, previousContext); - const start = Date.now(); - const analysis = await callGemini(prompt, geminiApiKey, metadata.number); + const prompt = await buildPrompt(issue, octokit, previousContext); + const startTime = Date.now(); + const analysis = await callGemini(prompt); + const analysisTimeSeconds = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`🤖 Gemini returned analysis in ${((Date.now() - start) / 1000).toFixed(1)}s with a human intervention rating of ${analysis.rating}/10:`); + console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a human intervention rating of ${analysis.rating}/10:`); console.log(`🤖 "${analysis.reason}"`); - await updateLabels(issue, analysis.labels, owner, repo, octokit); + await updateLabels(analysis.labels, octokit); if (analysis.comment) { console.log(`💬 Posting comment:`); console.log(analysis.comment.replace(/^/gm, '> ')); - await addComment(issue, analysis.comment, owner, repo, octokit); + await createComment(analysis.comment, octokit); } if (analysis.close) { - await closeIssue(issue, { owner, repo }, octokit, 'not_planned'); + await closeIssue(octokit, 'not_planned'); } if (analysis.newTitle) { - await updateTitle(issue, analysis.newTitle, owner, repo, octokit); + await updateTitle(issue.title, analysis.newTitle, octokit); } return analysis; } -/** - * Get previous triage context for an issue from the database - */ -function getPreviousContextForIssue(triageDb, issueNumber, issue) { - const triageEntry = triageDb[issueNumber]; +// Get previous triage context for re-triage conditions +function getPreviousContextForIssue(triageDb, issue) { + const triageEntry = triageDb[GITHUB_ISSUE_NUMBER]; // 1. Triage if it's never been checked. if (!triageEntry) { @@ -356,7 +255,8 @@ function getPreviousContextForIssue(triageDb, issueNumber, issue) { // --- Define conditions for re-triaging --- const MS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000 - const timeSinceTriaged = Date.now() - new Date(triageEntry.lastTriaged).getTime(); + const lastTriagedDate = new Date(triageEntry.lastTriaged); + const timeSinceTriaged = Date.now() - lastTriagedDate.getTime(); // 2. Triage if it's been > 14 days since the last check. const hasExpired = timeSinceTriaged > 14 * MS_PER_DAY; @@ -367,86 +267,63 @@ function getPreviousContextForIssue(triageDb, issueNumber, issue) { (labels.includes('info required') || labels.includes('stale')) && timeSinceTriaged > 3 * MS_PER_DAY; + // 4. Triage if the issue was updated since last triage + const issueUpdatedDate = new Date(issue.updated_at); + const wasUpdatedSinceTriaged = issueUpdatedDate > lastTriagedDate; + // If any condition for re-triaging is met, return the context. - if (hasExpired || needsFollowUp) { + if (hasExpired || needsFollowUp || wasUpdatedSinceTriaged) { return { lastTriaged: triageEntry.lastTriaged, previousReasoning: triageEntry.previousReasoning || 'No previous reasoning available.', }; } - // Otherwise, no triage is needed. - return null; + return null; // Otherwise, no triage is needed. } -/** - * Write contents to an artifact file - */ function saveArtifact(name, contents) { const artifactsDir = path.join(process.cwd(), 'artifacts'); - if (!fs.existsSync(artifactsDir)) { - fs.mkdirSync(artifactsDir); - } - const filePath = path.join(artifactsDir, name); + const filePath = path.join(artifactsDir, `${GITHUB_ISSUE_NUMBER}-${name}`); + fs.mkdirSync(artifactsDir, { recursive: true }); fs.writeFileSync(filePath, contents, 'utf8'); } -/** - * Main entry point - */ async function main() { - const requiredEnvVars = ['GITHUB_ISSUE_NUMBER', 'GEMINI_API_KEY', 'GITHUB_REPOSITORY']; - for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - throw new Error(`Missing required environment variable: ${envVar}`); - } - } - - const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); - const issueNumber = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); - const geminiApiKey = process.env.GEMINI_API_KEY; - - let octokit = null; - if (process.env.GITHUB_TOKEN) { - octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); - - const rate = await octokit.rest.rateLimit.get(); - if (rate.data.rate.remaining < 1000) { - console.log(`⚠️ GitHub API calls left: ${rate.data.rate.remaining} (resets at ${new Date(rate.data.rate.reset * 1000).toLocaleString()})`); - } else if (rate.data.rate.remaining < 500) { - console.log('❌ Too few GitHub API calls left, ending early to avoid hitting rate limit'); - process.exit(1); - } - } else { - console.log('⚠️ No GITHUB_TOKEN provided - running in read-only mode'); + for (const envVar of ['GITHUB_ISSUE_NUMBER', 'GEMINI_API_KEY', 'GITHUB_REPOSITORY', 'GITHUB_TOKEN']) { + if (!process.env[envVar]) throw new Error(`Missing environment variable: ${envVar}`); } + // Initialize database let triageDb = {}; - - if (dbPath && fs.existsSync(dbPath)) { - const contents = fs.readFileSync(dbPath, 'utf8'); + if (DB_PATH && fs.existsSync(DB_PATH)) { + const contents = fs.readFileSync(DB_PATH, 'utf8'); triageDb = contents ? JSON.parse(contents) : {}; } - const issue = await getIssueFromGitHub(owner, repo, issueNumber, octokit); - - const previousContext = getPreviousContextForIssue(triageDb, issueNumber, issue); + // Setup + const octokit = new Octokit({ auth: GITHUB_TOKEN }); + const issue = (await octokit.rest.issues.get(issueParams)).data; + const previousContext = getPreviousContextForIssue(triageDb, issue); + // Cancel early if (!previousContext) { - console.log(`⏭️ #${issueNumber} does not need to be triaged yet`); + console.log(`⏭️ #${GITHUB_ISSUE_NUMBER} does not need to be triaged right now`); process.exit(2); } + // Take action on issue console.log("⏭️"); - console.log(`🤖 Using ${aiModel} with [${Array.from(permissions).join(', ') || 'none'}] permissions`); - const analysis = await processIssue(issue, owner, repo, geminiApiKey, octokit, previousContext); + console.log(`🤖 Using ${AI_MODEL} with [${Array.from(PERMISSIONS).join(', ') || 'none'}] permissions`); + const analysis = await processIssue(issue, octokit, previousContext); - if (dbPath && analysis && !permissions.has("none")) { - triageDb[issueNumber] = { + // Save database + if (DB_PATH && analysis && PERMISSIONS.size > 0) { + triageDb[GITHUB_ISSUE_NUMBER] = { lastTriaged: new Date().toISOString(), previousReasoning: analysis.reason }; - fs.writeFileSync(dbPath, JSON.stringify(triageDb, null, 2)); + fs.writeFileSync(DB_PATH, JSON.stringify(triageDb, null, 2)); } } diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index d4e74ff8ce42..05fc355d3ca9 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -11,10 +11,11 @@ For testing purposes, don't consider "danielchalmers" a "repository collaborator ## PERSONA GUIDELINES Your role is an issue management assistant. Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. +You are here to help users and encourage constructive participation. Be supportive and positive, especially when guiding users to provide more information or improve their reports. You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. If a user provides a well-reasoned argument or new information that challenges your previous triage decision (like label assignment, a comment, or a triage rating), you must evaluate their input in the full context of the issue. If their argument is valid, you should recognize it by acknowledging your mistake, thanking them for the clarification or new details, or updating your decision as appropriate. -Your tone should be kind, helpful, and direct. +Your tone should be kind, encouraging, helpful, and direct. Don't overstep your bounds with statements which imply a level of self-confidence and authority like: - "This seems like a useful proposal" diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 44606e4c6d93..2237ffd1ec09 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -23,7 +23,9 @@ on: jobs: auto-triage: runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || vars.AUTOTRIAGE_PERMISSIONS # Run manually or on a schedule if permissions are set up. + # Safety switch: Scheduled runs require the AUTOTRIAGE_PERMISSIONS repo variable to be set. + if: github.event_name == 'workflow_dispatch' || vars.AUTOTRIAGE_PERMISSIONS + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -36,14 +38,15 @@ jobs: - name: Install dependencies run: npm install node-fetch@2 @actions/core @octokit/rest - - name: Cache triage database + # Persist the processed issues database (triage-db.json) between runs. + - name: Cache Triage Database uses: actions/cache@v4 id: cache-db with: path: triage-db.json - # 1. The key is unique on every run, forcing a save on success. + # Unique key forces a new cache save on each successful run. key: ${{ runner.os }}-triage-database-${{ github.run_id }} - # 2. Restore-keys finds the most recent cache to use at the start. + # Restore key prefix finds the latest available cache. restore-keys: | ${{ runner.os }}-triage-database- @@ -51,17 +54,20 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_PERMISSIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.permissions || vars.AUTOTRIAGE_PERMISSIONS }} AUTOTRIAGE_DB_PATH: ${{ github.workspace }}/triage-db.json + # Use input for permissions, fallback to repo variable for scheduled runs. + AUTOTRIAGE_PERMISSIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.permissions || vars.AUTOTRIAGE_PERMISSIONS }} + # Use a fixed limit for scheduled runs, fallback to manual input. MAX_ISSUES: ${{ github.event_name == 'schedule' && 50 || inputs.backlog-size }} run: | + # Process specified issues, or fetch from the backlog. if [ -n "${{ inputs.issue-numbers }}" ]; then echo "Processing specified issues: ${{ inputs.issue-numbers }}" issue_numbers="${{ inputs.issue-numbers }}" max_count="" else echo "Processing up to $MAX_ISSUES issues from the backlog" - issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc -label:enhancement -label:"on hold"' --json number --jq '.[].number') + issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc -label:enhancement -label:"on hold" -is:locked' --json number --jq '.[].number') max_count="$MAX_ISSUES" fi @@ -73,9 +79,8 @@ jobs: export GITHUB_ISSUE_NUMBER="$issue_number" - # This structure tells the shell we are handling the exit code ourselves + # Handle script exit codes: 0=Success, 1=Fatal Error, 2=Skip Issue. if node .github/scripts/AutoTriage.js; then - # SUCCESS (exit code 0) count=$((count + 1)) if [ -n "$max_count" ]; then left=$((max_count - count)) @@ -83,37 +88,21 @@ jobs: fi sleep 10 # Rate limit else - # FAILURE (exit code is non-zero) exit_code=$? if [ "$exit_code" -eq 1 ]; then - # Fatal error, fail the entire workflow - echo "❌ Fatal error while processing #${issue_number}" + echo "❌ Fatal error on #${issue_number}" exit 1 elif [ "$exit_code" -eq 2 ]; then - # Skippable issue. Do nothing and let the loop continue. - # The step will not be marked as failed. + # No-op for skippable issues. : else - # Any other unexpected error - echo "❌ Unexpected error (exit code $exit_code) for #${issue_number}" + echo "❌ Unexpected error (code $exit_code) on #${issue_number}" exit "$exit_code" fi fi done - - name: Display database stats - if: always() - run: | - if [ -f "triage-db.json" ]; then - total_entries=$(jq 'length' triage-db.json) - echo "Database now contains $total_entries triaged issues" - echo "Most recent entries:" - jq -r 'to_entries | sort_by(.value.lastTriaged) | reverse | .[0:5] | .[] | " #\(.key): \(.value.lastTriaged)"' triage-db.json || echo " No entries found" - else - echo "Database file not found" - fi - - - name: Upload triage artifacts + - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: triage-artifacts From 0414900ffd871d05561d4d152888ddd70b6e3ce1 Mon Sep 17 00:00:00 2001 From: Anu6is Date: Sat, 26 Jul 2025 08:32:33 -0400 Subject: [PATCH 15/43] MudHighlighter: Maintain markup formatting when Markup=true (#11705) --- .../Highlighter/BasicHighlighterTest.razor | 30 +++ .../Components/HighlighterTests.cs | 218 ++++++++++-------- .../Highlighter/MudHighlighter.razor | 72 +++--- 3 files changed, 187 insertions(+), 133 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/BasicHighlighterTest.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/BasicHighlighterTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/BasicHighlighterTest.razor new file mode 100644 index 000000000000..291b76b4dd72 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Highlighter/BasicHighlighterTest.razor @@ -0,0 +1,30 @@ + + + + +@code { + public static string __description__ = "Various text and search options"; + + [Parameter] + public string Text { get; set; } = "This is a test text for highlighting."; + + [Parameter] + public string HighlightedText { get; set; } = "test"; + + [Parameter] + public bool UntilNextBoundary { get; set; } = false; + + [Parameter] + public bool CaseSensitive { get; set; } = false; + + [Parameter] + public bool Markup { get; set; } = true; + + [Parameter] + public string? CustomClass { get; set; } +} diff --git a/src/MudBlazor.UnitTests/Components/HighlighterTests.cs b/src/MudBlazor.UnitTests/Components/HighlighterTests.cs index f7b27dcb4e54..12dd591ff57b 100644 --- a/src/MudBlazor.UnitTests/Components/HighlighterTests.cs +++ b/src/MudBlazor.UnitTests/Components/HighlighterTests.cs @@ -1,6 +1,7 @@ using Bunit; using FluentAssertions; using MudBlazor.Components.Highlighter; +using MudBlazor.UnitTests.TestComponents.Highlighter; using NUnit.Framework; using static Bunit.ComponentParameterFactory; using static MudBlazor.Components.Highlighter.Splitter; @@ -138,7 +139,7 @@ public void GetHtmlAwareFragments_NullOrEmptyText_ReturnsEmptyList() // Test with empty text var resultEmpty = GetHtmlAwareFragments(string.Empty, "any", null, out outRegex, false, false); resultEmpty.Should().BeEmpty(); - // As per L59, regex is set to string.Empty at the start of the method before the null check. + outRegex.Should().Be(string.Empty); } @@ -200,7 +201,7 @@ public void GetHtmlAwareFragments_MismatchedClosingTag_IsEncodedAsText() [Test] public void GetHtmlAwareFragments_UnmatchedTagWithHighlightAndTrailingText_CorrectlyFragments() { - var text = "unclosed highlight then_text"; // Simplified input + var text = "unclosed highlight then_text"; var highlightedText = "highlight"; var fragments = GetHtmlAwareFragments(text, highlightedText, null, out var outRegex, caseSensitive: false, untilNextBoundary: false); @@ -404,173 +405,202 @@ public void MudHighlighterMarkupRenderFragmentTest() var rawOutput = "<i>MudBlazor</i>"; var formattedOutput = "MudBlazor"; - var text = Parameter(nameof(MudHighlighter.Text), markupText); - var highlightedText = Parameter(nameof(MudHighlighter.HighlightedText), searchFor); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, markupText) + .Add(p => p.HighlightedText, searchFor) + .Add(p => p.Markup, false) + ); - var textAsMarkupFalse = Parameter(nameof(MudHighlighter.Markup), false); - var comp = Context.RenderComponent(text, highlightedText, textAsMarkupFalse); - comp.MarkupMatches(rawOutput); + comp.Markup.Should().Contain(rawOutput); - var textAsMarkupTrue = Parameter(nameof(MudHighlighter.Markup), true); - comp = Context.RenderComponent(text, highlightedText, textAsMarkupTrue); - comp.MarkupMatches(formattedOutput); + comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, markupText) + .Add(p => p.HighlightedText, searchFor) + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain(formattedOutput); } [Test] public void MudHighlighter_MarkupTrue_HtmlInText_ShouldHighlightCorrectly() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello World"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "Hello"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello World"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello World") + .Add(p => p.HighlightedText, "Hello") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello World"); } [Test] public void MudHighlighter_MarkupTrue_HtmlSensitiveCharInHighlightedText_ShouldEncodeAndHighlight() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello "); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello <World>"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello ") + .Add(p => p.HighlightedText, "") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello <World>"); } [Test] public void MudHighlighter_MarkupTrue_HtmlInText_ShouldNotHighlightInTags() { - var textParam = Parameter(nameof(MudHighlighter.Text), "
div content div
"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "div"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("
div content div
"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "
div content div
") + .Add(p => p.HighlightedText, "div") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("
div content div
"); } [Test] public void MudHighlighter_MarkupTrue_HtmlTag_ShouldNotHighlight() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello Mud World"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello Mud World"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello Mud World") + .Add(p => p.HighlightedText, "") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello Mud World"); } [Test] public void MudHighlighter_MarkupTrue_TextWithHtmlEntities_HighlightedTextIsEntityText() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello & World"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "&"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - // Adjusted to match BUnit's actual output. This implies that when FragmentInfo.Content = "&", - // the rendered @FragmentInfo.Content is captured by BUnit as &amp;. - comp.MarkupMatches("Hello &amp; World"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello & World") + .Add(p => p.HighlightedText, "&") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello &amp; World"); } [Test] public void MudHighlighter_MarkupTrue_HighlightedTextWithSingleQuotes_ShouldHighlight() { - var textParam = Parameter(nameof(MudHighlighter.Text), "This is a 'quoted' text and a \"double quoted\" text."); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "'quoted'"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("This is a 'quoted' text and a \"double quoted\" text."); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "This is a 'quoted' text and a \"double quoted\" text.") + .Add(p => p.HighlightedText, "'quoted'") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("This is a 'quoted' text and a "double quoted" text."); } [Test] public void MudHighlighter_MarkupTrue_HighlightedTextWithDoubleQuotes_ShouldHighlight() { - var textParam = Parameter(nameof(MudHighlighter.Text), "This is a 'quoted' text and a \"double quoted\" text."); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "\"double quoted\""); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("This is a 'quoted' text and a "double quoted" text."); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "This is a 'quoted' text and a \"double quoted\" text.") + .Add(p => p.HighlightedText, "\"double quoted\"") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("This is a 'quoted' text and a "double quoted" text."); } [Test] public void MudHighlighter_MarkupTrue_HighlightedTextAsAttributeValue_ShouldNotHighlightInAttribute() { - var textParam = Parameter(nameof(MudHighlighter.Text), "nothing"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "nothing"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("nothing"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "MudBlazor is important") + .Add(p => p.HighlightedText, "nothing") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("MudBlazor is important"); } [Test] public void MudHighlighter_MarkupTrue_FormattingPreservation_ItalicsAndColor() { - var textParam = Parameter(nameof(MudHighlighter.Text), "MudBlazor is important"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "MudBlazor"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("MudBlazor is important"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "MudBlazor is important") + .Add(p => p.HighlightedText, "MudBlazor") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("MudBlazor is important"); } [Test] public void MudHighlighter_MarkupTrue_FormattingPreservation_Bold() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Normal bold normal"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "bold"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Normal bold normal"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Normal bold normal") + .Add(p => p.HighlightedText, "bold") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Normal bold normal"); } [Test] public void MudHighlighter_MarkupTrue_NonStandardTag_NoHighlight() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), ""); // Or null, effectively no highlight - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello <ambitious> world"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello world") + .Add(p => p.HighlightedText, "") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello <ambitious> world"); } [Test] public void MudHighlighter_MarkupTrue_NonStandardTag_WithHighlightAfterTag() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "world"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello <ambitious> world"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello world") + .Add(p => p.HighlightedText, "world") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello <ambitious> world"); } [Test] public void MudHighlighter_MarkupTrue_NonStandardTag_WithHighlightInsideTag() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Hello world"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "bit"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); - comp.MarkupMatches("Hello <ambitious> world"); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Hello world") + .Add(p => p.HighlightedText, "bit") + .Add(p => p.Markup, true) + ); + + comp.Markup.Should().Contain("Hello <ambitious> world"); } [Test] public void MudHighlighter_MarkupTrue_NoFragments_RendersTextAsMarkupString() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Some text"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "zip"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Some text") + .Add(p => p.HighlightedText, "zip") + .Add(p => p.Markup, true) + ); - comp.MarkupMatches("Some text"); + comp.Markup.Should().Contain("Some text"); } [Test] public void MudHighlighter_MarkupTrue_WithClass_RendersMarkWithClass() { - var textParam = Parameter(nameof(MudHighlighter.Text), "Highlight this"); - var highlightedTextParam = Parameter(nameof(MudHighlighter.HighlightedText), "Highlight"); - var markupParam = Parameter(nameof(MudHighlighter.Markup), true); - var classParam = Parameter(nameof(MudHighlighter.Class), "my-custom-class"); - - var comp = Context.RenderComponent(textParam, highlightedTextParam, markupParam, classParam); + var comp = Context.RenderComponent(parameters => parameters + .Add(p => p.Text, "Highlight this") + .Add(p => p.HighlightedText, "Highlight") + .Add(p => p.Markup, true) + .Add(p => p.CustomClass, "my-custom-class") + ); - comp.MarkupMatches("Highlight this"); + comp.Markup.Should().Contain("Highlight this"); } [Test] @@ -580,23 +610,21 @@ public void MudHighlighter_MarkupFalse_AfterMarkupTrue_ClearsHtmlAwareFragmentsA var initialHighlightedText = "highlight"; // 1. Render initially with Markup = true - var comp = Context.RenderComponent(parameters => parameters + var comp = Context.RenderComponent(parameters => parameters .Add(p => p.Text, initialText) .Add(p => p.HighlightedText, initialHighlightedText) .Add(p => p.Markup, true) ); - comp.MarkupMatches("Test with HTML and highlight"); + comp.Markup.Should().Contain("Test with HTML and highlight"); // 2. Re-render with Markup = false comp.SetParametersAndRender(parameters => parameters - .Add(p => p.Text, initialText) // Keep text and highlight the same - .Add(p => p.HighlightedText, initialHighlightedText) .Add(p => p.Markup, false) ); var expectedMarkup = "Test with <b>HTML</b> and highlight"; - comp.MarkupMatches(expectedMarkup); + comp.Markup.Should().Contain(expectedMarkup); } } } diff --git a/src/MudBlazor/Components/Highlighter/MudHighlighter.razor b/src/MudBlazor/Components/Highlighter/MudHighlighter.razor index b34629df01ea..cf0017ad5896 100644 --- a/src/MudBlazor/Components/Highlighter/MudHighlighter.razor +++ b/src/MudBlazor/Components/Highlighter/MudHighlighter.razor @@ -3,12 +3,34 @@ @using System.Text.RegularExpressions @using MudBlazor.Components.Highlighter @using System.Text +@using System.Net @if (Markup) { if (_htmlAwareFragments != null) { - @CreateFragmentContent(); + var sb = new StringBuilder(); + foreach (var fragmentInfo in _htmlAwareFragments) + { + switch (fragmentInfo.Type) + { + case FragmentType.HighlightedText: + var attributes = RenderAttributes(UserAttributes); + var classNames = string.IsNullOrWhiteSpace(Class) ? string.Empty : $" class=\"{Class}\" "; + var styles = string.IsNullOrWhiteSpace(Style) ? string.Empty : $" style=\"{Style}\" "; + sb.Append($"{WebUtility.HtmlEncode(fragmentInfo.Content)}"); + break; + case FragmentType.Markup: + sb.Append(fragmentInfo.Content); + break; + case FragmentType.Text: + default: + sb.Append(WebUtility.HtmlEncode(fragmentInfo.Content)); + break; + } + } + + @((MarkupString)sb.ToString()) } else if (!string.IsNullOrEmpty(Text)) { @@ -35,44 +57,18 @@ else if (!string.IsNullOrEmpty(Text)) } @code { - private RenderFragment CreateFragmentContent() => builder => + private string RenderAttributes(IDictionary attributes) { - int sequence = 0; - - foreach (var fragmentInfo in _htmlAwareFragments) - { - switch (fragmentInfo.Type) - { - case FragmentType.HighlightedText: - if (IsMatch(fragmentInfo.Content)) - { - builder.OpenElement(sequence++, "mark"); - - if (!string.IsNullOrWhiteSpace(Class)) - builder.AddAttribute(sequence++, "class", Class); + if (attributes == null) return string.Empty; - if (!string.IsNullOrWhiteSpace(Style)) - builder.AddAttribute(sequence++, "style", Style); - - if (UserAttributes != null || UserAttributes.Count != 0) - builder.AddMultipleAttributes(sequence++, UserAttributes); - - builder.AddContent(sequence++, fragmentInfo.Content); - builder.CloseElement(); - } - else - { - builder.AddContent(sequence++, fragmentInfo.Content); - } - break; - case FragmentType.Markup: - builder.AddMarkupContent(sequence++, fragmentInfo.Content); - break; - case FragmentType.Text: - default: - builder.AddContent(sequence++, fragmentInfo.Content); - break; - } + var sb = new StringBuilder(); + foreach (var kvp in attributes) + { + var attrName = System.Net.WebUtility.HtmlEncode(kvp.Key); + var attrValue = System.Net.WebUtility.HtmlEncode(kvp.Value?.ToString()); + sb.Append($" {attrName}=\"{attrValue}\" "); } - }; + + return sb.ToString(); + } } From 056ddccbadf2ca7c2fbbe131b03c61ca81f5078a Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sat, 26 Jul 2025 13:41:12 -0500 Subject: [PATCH 16/43] Build: Run AutoTriage more often, Add Discord pings, Other refinements (#11716) --- .github/scripts/AutoTriage.js | 26 +++++++++-- .github/scripts/AutoTriage.prompt | 67 +++++++++++++++------------- .github/workflows/issue.yml | 1 + .github/workflows/pr.yml | 1 + .github/workflows/triage-backlog.yml | 15 ++++--- 5 files changed, 69 insertions(+), 41 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index a73920869f08..d380e07fcf09 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -20,8 +20,10 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; const GITHUB_ISSUE_NUMBER = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); +const AUTOTRIAGE_WEBHOOK = process.env.AUTOTRIAGE_WEBHOOK; const [OWNER, REPO] = (GITHUB_REPOSITORY || '').split('/'); const issueParams = { owner: OWNER, repo: REPO, issue_number: GITHUB_ISSUE_NUMBER }; +const GITHUB_ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues/${GITHUB_ISSUE_NUMBER}`; // Allowed actions: 'label', 'comment', 'close', 'edit'; 'none' disables all actions. let PERMISSIONS = new Set( @@ -34,6 +36,19 @@ if (PERMISSIONS.has('none')) PERMISSIONS.clear(); const can = action => PERMISSIONS.has(action); +// Send a Discord alert for urgent issues +async function sendAlert(issue, reason) { + if (!AUTOTRIAGE_WEBHOOK || !can('alert')) return; + await fetch(AUTOTRIAGE_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: `**🚨 Potentially Urgent — ${issue.title}**\n${reason}\n${GITHUB_ISSUE_URL}` + }) + }); + console.log(`🚨 Webhook alert sent: ${AUTOTRIAGE_WEBHOOK}`); +} + // Call Gemini to analyze the issue content and return structured response async function callGemini(prompt) { const payload = { @@ -66,7 +81,7 @@ async function callGemini(prompt) { ); if (!response.ok) { - throw new Error(`Gemini API error: ${response.status} ${response.statusText} — ${await response.text()}`); + throw new Error(`Gemini: ${response.status} ${response.statusText} — ${await response.text()}`); } const data = await response.json(); @@ -75,7 +90,6 @@ async function callGemini(prompt) { saveArtifact('gemini-output.json', JSON.stringify(data, null, 2)); saveArtifact('gemini-analysis.json', result); - if (!result) throw new Error('No analysis result in Gemini response'); return JSON.parse(result); } @@ -151,6 +165,8 @@ ${JSON.stringify(timelineReport, null, 2)} Last triaged: ${previousContext?.lastTriaged} Previous reasoning: ${previousContext?.previousReasoning} Current triage date: ${new Date().toISOString()} +Current permissions: ${Array.from(PERMISSIONS).join(', ') || 'none'} +All possible permissions: label (add/remove labels), comment (post comments), close (close issue), edit (edit title), alert (send Discord notification) === SECTION: INSTRUCTIONS === Analyze this issue, its metadata, and its full timeline. Your entire response must be a single, valid JSON object and nothing else. Do not use Markdown, code fences, or any explanatory text.`; @@ -225,6 +241,10 @@ async function processIssue(issue, octokit, previousContext = null) { console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a human intervention rating of ${analysis.rating}/10:`); console.log(`🤖 "${analysis.reason}"`); + if (analysis.rating >= 9) { + await sendAlert(issue, analysis.reason); + } + await updateLabels(analysis.labels, octokit); if (analysis.comment) { @@ -308,7 +328,7 @@ async function main() { // Cancel early if (!previousContext) { - console.log(`⏭️ #${GITHUB_ISSUE_NUMBER} does not need to be triaged right now`); + //console.log(`⏭️ #${String(GITHUB_ISSUE_NUMBER).padStart(5, '0')} does not need to be triaged right now`); process.exit(2); } diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 05fc355d3ca9..8b6516dcc9e8 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -6,7 +6,7 @@ On GitHub, your username is `github-actions` or `github-actions[bot]`. Analyze the issue and return the structured JSON format. Explain your reasoning in `reason` in the JSON response. If you wanted to make a change but didn't because of a rule, you can mention that. All time-based calculations (such as age, stale, or inactivity rules) must be performed by comparing the relevant created or activity date (e.g., issue/PR/comment/label) to the "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. -For testing purposes, don't consider "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. +For testing purposes, don't consider the user "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. ## PERSONA GUIDELINES @@ -52,7 +52,7 @@ Don't overstep your bounds with statements which imply a level of self-confidenc "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug" "urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" "info required": "Indicates the issue is blocked pending necessary details from the author for triage" -"invalid": "Action label indicating a violation of community standards that warrants closing the issue, or an issue that will be closed due to excessive low quality" +"invalid": "Action label indicating blatant spam or a violation of community standards" "localization": "Concerns support for multiple languages or regional formats" "mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android)" "needs example": "Specific missing information is a code example or a reproduction link" @@ -81,8 +81,10 @@ When to apply specific labels:     - You must always post a comment explaining what information is needed when applying this label. - 'needs example': Apply this in addition to 'info required' when the specific missing information is a code example or a reproduction link. - 'needs screenshot': Apply this in addition to 'info required' when the specific missing information is a screenshot or video of the visual problem. -- 'invalid': For extremely low-quality issues that are empty, unintelligible, or contain no actionable information (e.g., a title with a blank body, or a body that just says "doesn't work"). - - In this specific case, apply `invalid` *instead of* `info required`. +- 'invalid': + - Apply only to extremely low-quality issues that are empty, unintelligible, or spam. + - Never mark an issue as 'invalid' if it reports a security vulnerability, exploit, or any actionable technical concern, even if the report is incomplete or informally worded. + - If the issue has any substantive information, apply 'info required' instead and request the missing details. - You must always post a comment explaining why the issue was marked invalid. - 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. You must always post a comment explaining why the label was added and direct to appropriate community channels. - 'regression': Apply this in addition to 'bug' to indicate a high-priority bug where a feature that previously worked is now broken. @@ -129,13 +131,13 @@ Your primary goal is to gather information for maintainers efficiently. Only com **Prioritize Commenting For:** - **Immediate Triage Needs (Human Intervention Rating 9-10):** - - **Always comment on an issue** to flag critical items (e.g., security vulnerabilities, severe regressions). Your comment should include `cc @MudBlazor/triage`. + - Always comment on an issue to flag critical items (e.g., security vulnerabilities, severe regressions). Your comment should include `cc @MudBlazor/triage`. - **Missing Information for Triage (`info required`):** - If the `info required` label is added to an issue, you must ALWAYS leave a comment explaining what information is missing and why it is needed for triage. - - If key details are missing in an **issue** (e.g., reproduction, browser, version, screenshots, error messages, clear use case), **you must comment** explaining what's needed. - - **Always add the `info required` label** in this scenario, if not already present. + - If key details are missing in an issue (e.g., reproduction, browser, version, screenshots, error messages, clear use case), **you must comment** explaining what's needed. + - Always add the `info required` label in this scenario, if not already present. - **Usage Questions (`question` label):** - - If an **issue** is clearly a help request and not a bug/feature, **you must comment** to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). + - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** - **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. - **Common Troubleshooting Suggestions:** @@ -170,7 +172,7 @@ Explain *why* you need information and frame advice as possibilities based on ge Don't attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. Use "For anyone investigating this..." instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. -For conduct violations: Be firm, explain the violation, link to the Code of Conduct, and immediately close the issue. +For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" **Examples:** @@ -188,7 +190,6 @@ Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed som - "This appears to be a regression that could affect many users. cc @MudBlazor/triage" - "This was reported against an older MudBlazor version. Could you test if it still occurs with the latest version?" - "Could you verify if this problem still exists using your provided reproduction link? Our snippet editor at try.mudblazor.com always runs the latest version, so you can just open the link and check." -- "This issue has been marked as invalid because it does not contain a description of the problem. To be actionable, a report needs details explaining the expected behavior versus the actual behavior. Please edit this issue to provide that information." - "MudDataGrid has received updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" Avoid using the examples provided in this prompt verbatim. The examples are for guidance on tone and content. They can be similar but try to rephrase the examples using your own words to fit the specific issue context. @@ -196,8 +197,8 @@ Avoid using the examples provided in this prompt verbatim. The examples are for ## ISSUE CLOSING POLICY You must never close an issue unless: -- It has been marked as `stale` and meets all the rules for closing stale issues (see below), -- OR it is a code of conduct violation (see above). +- It has been marked as `invalid` +- It has been marked as `stale` AND meets all the rules for closing stale issues (see below) Don't close issues for any other reason, even if they're low quality, invalid, or missing information. Only comment or label in those cases. They will be allowed to go stale and then closed later. @@ -262,18 +263,17 @@ If you would like to continue working on it, please open a new PR referencing th ## EDITING GUIDELINES -Suggest a `newTitle` in the JSON **only if** the current title is **very unclear** and the issue is **older than one week with low activity**. +Suggest a `newTitle` in the JSON if the current title is very unclear or unrelated and the issue is older than one week with low activity. +- Never edit issues by repository collaborators. +- Take note of previous "renamed" events. - Keep changes minimal. -- Never edit titles by repository collaborators. -- Titles should be **sentence-cased**. -- Titles must clearly state the author's request. +- Titles should be sentence-cased. - Prioritize clarity and searchability. -- Only edit if it's a significant improvement. ### Pull Request Title Format -PR titles must be prefixed with the relevant component. +PR titles must be prefixed with the relevant component (colon format). **Examples:** - MudDialog: Fix gaps in body style @@ -281,7 +281,8 @@ PR titles must be prefixed with the relevant component. ### Issue Title Format -Issue titles must NOT use component prefixes (e.g., "MudButton:") or include version numbers. +It's optional for issue titles to use the colon format (e.g., "MudButton: ..."), but you prefer not to use it when creating a new title. +Issue titles don't need to include version numbers or labels that are already present in the body. **Examples:** - Make overlay popovers non-modal by default @@ -293,20 +294,22 @@ Issue titles must NOT use component prefixes (e.g., "MudButton:") or include ver The **human intervention rating** indicates how critical it is for a maintainer to address an issue quickly. This rating, on a scale of **1 to 10**, is based on factors such as: -- **Security Vulnerabilities:** Issues that represent a direct security risk to users or the project. These will typically receive a higher rating (**8-10**). The higher end is for actively exploitable vulnerabilities. -- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component or a large number of users. The severity and impact of the regression will influence the rating (**6-10**). A regression that crashes the app for many users is higher than a minor visual regression. -- **Widespread Impact:** Issues affecting a broad user base or a fundamental part of the framework (**7-9**). This can be a new bug or a critical missing feature. -- **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development (**6-8**). This often means there's no easy workaround. -- **Clarity and Reproducibility:** Well-documented issues with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7** until clarity is achieved). This rewards good reporting. -- **Community Engagement:** High community interest (e.g., many reactions, comments from diverse users) can subtly increase the priority of an issue that isn't inherently critical (e.g., a popular enhancement request might be a **4-6**, whereas a less popular one is a **1-3**). This indicates broader desire. -- **Age of Issue:** Older issues generally have a lower intervention priority. If an issue has gone unaddressed for an extended period without new activity, it typically suggests less urgency or widespread impact, lowering its rating (**1-3** for very old, inactive issues). This prevents the backlog from being constantly re-prioritized by old items. +- **Security Vulnerabilities:** Issues representing a direct security risk (**8-10**). The higher end is for actively exploitable vulnerabilities. +- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component. The severity and impact of the regression will influence the rating (**6-10**). +- **Widespread Impact:** Bugs affecting a broad user base or a fundamental part of the framework (**7-9**). +- **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development, often with no easy workaround (**6-8**). +- **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. +- **Community Engagement:** High community interest (e.g., many reactions, comments) can subtly increase the priority of a bug that isn't inherently critical, indicating broader desire for a fix. +- **Age of Issue:** Very old, inactive bug reports generally have a lower intervention priority, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. -Low-quality issues (e.g., empty, unintelligible, or extremely low-effort) do **not** warrant a high human intervention rating (9-10) or a triage ping (`cc @MudBlazor/triage`). These issues will be handled by the stale bot. +If an urgent intervention has already been applied (e.g., the `urgent` label exists or the triage team has been pinged), do not apply it again; assume that human intervention is already in progress or that the team is aware of the issue. + +Low-quality issues (e.g., empty, unintelligible, or extremely low-effort) do not warrant a high human intervention rating (9-10) or a triage ping (`cc @MudBlazor/triage`). These issues will be handled by the stale bot. ### Intervention Rating Scale -- **1-3 (Low Intervention):** Routine enhancements, minor documentation fixes, or very old, inactive issues with low impact. These can be triaged at leisure. *Examples: Cosmetic bug in an obscure component, request for a minor new prop, a year-old issue with no comments.* -- **4-6 (Moderate Intervention):** General bug reports with clear steps but not critical impact, clear enhancement requests with moderate impact, or issues requiring some investigation but not immediate action. *Examples: Component behaving slightly unexpectedly but with a workaround, a well-defined feature request for a commonly used component.* -- **7-8 (High Intervention):** Important bugs impacting several users, significant enhancements, or issues that, while not critical, should be addressed within a reasonable timeframe. *Examples: A bug that makes a widely used component difficult to use without a clear workaround, a feature that significantly improves developer experience or solves a common integration challenge.* -- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or issues causing severe application failures. If an issue warrants this rating, you should comment with "cc @MudBlazor/triage" to alert the triage team immediately. - - Apply the `urgent` label for issues or PRs with this rating. +- **0 (No Intervention Needed):** This rating is automatically assigned to issues labeled as `enhancement`. Feature requests are triaged based on community interest and alignment with the project roadmap, not on an urgency scale. +- **1-3 (Low Intervention):** Minor bugs with low impact. These can be triaged at leisure. *Examples: A cosmetic bug in an obscure component, a very old and inactive bug report with no recent community engagement.* +- **4-6 (Moderate Intervention):** General bug reports with clear reproduction steps but no critical impact. These issues require investigation but not immediate action. *Example: A component behaving unexpectedly but a functional workaround exists.* +- **7-8 (High Intervention):** Important bugs that impact several users or make a widely used component difficult to use without a clear workaround. These should be addressed in a reasonable timeframe. +- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. If an issue warrants this rating, you must ask for as much information as possible, comment with "cc @MudBlazor/triage" to alert the triage team, and apply the `urgent` label. diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 08ce9349080c..bf79494b2cca 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -23,6 +23,7 @@ jobs: GITHUB_ISSUE_NUMBER: ${{ github.event.issue.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ace901158162..1757045cd859 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,6 +23,7 @@ jobs: GITHUB_ISSUE_NUMBER: ${{ github.event.pull_request.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 2237ffd1ec09..d44bb93fecc4 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -2,7 +2,7 @@ name: Triage Backlog on: schedule: - - cron: '0 6 * * *' + - cron: '0 */8 * * *' workflow_dispatch: inputs: backlog-size: @@ -17,13 +17,13 @@ on: permissions: description: 'Permissions (`none` for dry run)' required: false - default: 'label, comment, close, edit' + default: 'label, comment, close, edit, alert' type: string jobs: auto-triage: runs-on: ubuntu-latest - # Safety switch: Scheduled runs require the AUTOTRIAGE_PERMISSIONS repo variable to be set. + # Safety: Scheduled runs require the AUTOTRIAGE_PERMISSIONS repo variable to be set. if: github.event_name == 'workflow_dispatch' || vars.AUTOTRIAGE_PERMISSIONS steps: @@ -54,11 +54,12 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} AUTOTRIAGE_DB_PATH: ${{ github.workspace }}/triage-db.json # Use input for permissions, fallback to repo variable for scheduled runs. AUTOTRIAGE_PERMISSIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.permissions || vars.AUTOTRIAGE_PERMISSIONS }} # Use a fixed limit for scheduled runs, fallback to manual input. - MAX_ISSUES: ${{ github.event_name == 'schedule' && 50 || inputs.backlog-size }} + MAX_ISSUES: ${{ github.event_name == 'schedule' && 25 || inputs.backlog-size }} # Gemini can handle 100 issues per day. run: | # Process specified issues, or fetch from the backlog. if [ -n "${{ inputs.issue-numbers }}" ]; then @@ -66,11 +67,13 @@ jobs: issue_numbers="${{ inputs.issue-numbers }}" max_count="" else - echo "Processing up to $MAX_ISSUES issues from the backlog" + echo "Analyzing up to $MAX_ISSUES issues" issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc -label:enhancement -label:"on hold" -is:locked' --json number --jq '.[].number') max_count="$MAX_ISSUES" fi + echo "Searching through $(echo \"$issue_numbers\" | wc -w) total issues from GitHub..." + count=0 for issue_number in $issue_numbers; do if [ -n "$max_count" ] && [ "$count" -ge "$max_count" ]; then @@ -86,7 +89,7 @@ jobs: left=$((max_count - count)) echo "⏭️ $(date '+%Y-%m-%d %H:%M:%S'): $left left" fi - sleep 10 # Rate limit + sleep 3 # Rate limit else exit_code=$? if [ "$exit_code" -eq 1 ]; then From 0f96fc080520a70299e675722d61a815a4c93759 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sat, 26 Jul 2025 15:09:32 -0500 Subject: [PATCH 17/43] Build: Delete performance template It's missing information that's covered in the bug report (like version and reproduction) so the AI is getting confused --- .../ISSUE_TEMPLATE/04_performance_issue.md | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/04_performance_issue.md diff --git a/.github/ISSUE_TEMPLATE/04_performance_issue.md b/.github/ISSUE_TEMPLATE/04_performance_issue.md deleted file mode 100644 index ecafbebdf1f6..000000000000 --- a/.github/ISSUE_TEMPLATE/04_performance_issue.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Performance issue -about: Report a performance problem or regression -title: '' -labels: 'performance' -assignees: '' - ---- - -### Description - - - -### Configuration - - - -### Data - - - -### Analysis - - From f2f8bace5eb65fd91cc30c0d2ed8a1961f4b6445 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sat, 26 Jul 2025 20:59:33 -0500 Subject: [PATCH 18/43] Update AutoTriage.prompt --- .github/scripts/AutoTriage.prompt | 113 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 8b6516dcc9e8..c492228133a9 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -1,28 +1,23 @@ # GitHub Issue Analysis Assistant -## CORE TASKS +## CORE BEHAVIOR On GitHub, your username is `github-actions` or `github-actions[bot]`. Analyze the issue and return the structured JSON format. Explain your reasoning in `reason` in the JSON response. If you wanted to make a change but didn't because of a rule, you can mention that. All time-based calculations (such as age, stale, or inactivity rules) must be performed by comparing the relevant created or activity date (e.g., issue/PR/comment/label) to the "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. For testing purposes, don't consider the user "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. +You do not have the ability to test reproduction links, run code, or verify if a bug is present in a live environment. Never imply, state, or reason that you have tested a reproduction link or confirmed a bug by running code. Do NOT lie about capabilities under any circumstances; you can only talk. +You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. +Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. ## PERSONA GUIDELINES -Your role is an issue management assistant. Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. +Your role is an issue management assistant. +Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. You are here to help users and encourage constructive participation. Be supportive and positive, especially when guiding users to provide more information or improve their reports. -You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. -Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. -If a user provides a well-reasoned argument or new information that challenges your previous triage decision (like label assignment, a comment, or a triage rating), you must evaluate their input in the full context of the issue. If their argument is valid, you should recognize it by acknowledging your mistake, thanking them for the clarification or new details, or updating your decision as appropriate. -Your tone should be kind, encouraging, helpful, and direct. - -Don't overstep your bounds with statements which imply a level of self-confidence and authority like: -- "This seems like a useful proposal" -- "it looks like the problem is indeed reproducible" -- "This helps pinpoint the nature of the performance issue" -- "It looks very similar to a bug that was previously fixed in #xxxx" -- "That seems like a sound, low-risk improvement that aligns with best practices" +Always evaluate new information or arguments from users even if they challenge your previous triage decision (like label assignment, a comment, or a triage rating). +Your tone should be encouraging, helpful, and direct. ## PROJECT CONTEXT @@ -30,7 +25,8 @@ Don't overstep your bounds with statements which imply a level of self-confidenc - Written in C#, Razor, and CSS with minimal JavaScript - Cross-platform support (Server, WebAssembly, MAUI) - Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they're still a concern without needing to update the package -- Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com (the generic placeholder link "[https://try.mudblazor.com/snippet](https://try.mudblazor.com/snippet)" counts as a missing reproduction) +- Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com + - the generic placeholder link "https://try.mudblazor.com/snippet" with nothing after "snippet" counts as a missing reproduction. It should look like "https://try.mudblazor.com/snippet/GOcpOVQqhRGrGiGV". - Current v8.x.x supports .NET 8 and later - Version migration guides are at [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) - Templates for new projects are at [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) @@ -41,6 +37,7 @@ Don't overstep your bounds with statements which imply a level of self-confidenc ## VALID LABELS +**General labels:** "accessibility": "Impacts usability for users with disabilities (a11y)" "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update" "bug": "An unexpected behavior or defect. Primary issue type." @@ -50,7 +47,6 @@ Don't overstep your bounds with statements which imply a level of self-confidenc "enhancement": "A new feature or improvement. Primary issue type." "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors" "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug" -"urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" "info required": "Indicates the issue is blocked pending necessary details from the author for triage" "invalid": "Action label indicating blatant spam or a violation of community standards" "localization": "Concerns support for multiple languages or regional formats" @@ -66,6 +62,13 @@ Don't overstep your bounds with statements which imply a level of self-confidenc "security": "Impacts application security, including vulnerabilities or data protection" "stale": "Indicates an issue is inactive and will be closed if no further updates occur" "tests": "Relates to unit, integration, or other automated testing frameworks" +"urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" + +**Labels that only human maintainers can apply:** +"answered": "Indicates the issue has been answered and does not require further action" +"not planned": "Indicates the issue or feature request is not planned for implementation" +"duplicate": "Indicates the issue is a duplicate of another issue" +"fixed": "Indicates the issue has been resolved or fixed in a recent update" ## LABELING GUIDELINES @@ -78,15 +81,15 @@ When to apply specific labels: - 'info required':     - If you're uncertain of the primary issue type ('bug', 'enhancement', 'docs') initially, apply 'info required'.     - This label can remain alongside a primary issue type if other crucial details are still missing for full triage after the primary type has been identified. -    - You must always post a comment explaining what information is needed when applying this label. +    - Always post a comment explaining what information is needed when applying this label. - 'needs example': Apply this in addition to 'info required' when the specific missing information is a code example or a reproduction link. - 'needs screenshot': Apply this in addition to 'info required' when the specific missing information is a screenshot or video of the visual problem. - 'invalid': - Apply only to extremely low-quality issues that are empty, unintelligible, or spam. - Never mark an issue as 'invalid' if it reports a security vulnerability, exploit, or any actionable technical concern, even if the report is incomplete or informally worded. - If the issue has any substantive information, apply 'info required' instead and request the missing details. - - You must always post a comment explaining why the issue was marked invalid. -- 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. You must always post a comment explaining why the label was added and direct to appropriate community channels. + - Always post a comment explaining why the issue was marked invalid. +- 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. Always post a comment explaining why the label was added and direct to appropriate community channels. - 'regression': Apply this in addition to 'bug' to indicate a high-priority bug where a feature that previously worked is now broken. ## COMMON ISSUE TYPES @@ -113,7 +116,7 @@ Consider an issue's age and update frequency. Note engagement like comments and - Missing expected vs actual behavior - Missing technical details (version, browser, render mode) - Ambiguous or unhelpful issue titles -- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be very certain it's a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question +- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be certain it's a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question - Code of conduct violations (harassment, trolling, personal attacks) - Extremely low-effort issues (single words, gibberish, spam) - Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work") @@ -130,19 +133,20 @@ Consider an issue's age and update frequency. Note engagement like comments and Your primary goal is to gather information for maintainers efficiently. Only comment when necessary to move an issue forward. **Prioritize Commenting For:** -- **Immediate Triage Needs (Human Intervention Rating 9-10):** - - Always comment on an issue to flag critical items (e.g., security vulnerabilities, severe regressions). Your comment should include `cc @MudBlazor/triage`. +- **Human Needed:** +   - When you assign a rating of 7 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage`. +   - In the comment, explain why the issue needs maintainer intervention and what you needs help with. - **Missing Information for Triage (`info required`):** - If the `info required` label is added to an issue, you must ALWAYS leave a comment explaining what information is missing and why it is needed for triage. - - If key details are missing in an issue (e.g., reproduction, browser, version, screenshots, error messages, clear use case), **you must comment** explaining what's needed. + - If key details are missing in an issue (e.g., reproduction, browser, version, screenshots, error messages, clear use case, still present in latest version), you must comment explaining what's needed. - Always add the `info required` label in this scenario, if not already present. - **Usage Questions (`question` label):** - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** - **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. - **Common Troubleshooting Suggestions:** - - If a bug report involves a component that is frequently updated, it's possible the issue has already been fixed. **You must comment** to ask the user to verify the problem on the latest version. - - **Components:** `MudDataGrid`, `MudTable`, `MudTextField`, `MudSelect`, `MudAutocomplete`, `MudInput`, `MudDateRangePicker`, `MudPicker`, `MudMenu`, `MudPopover`, `MudDialog`. + - If a bug report involves a component that is frequently updated, it's possible the issue has already been fixed. Comment to ask the user to verify the problem on the latest version. + - **Components:** `MudDataGrid`, `MudTable`, `MudTabs`, `MudSelect`, `MudAutocomplete`, `MudPicker`, `MudMenu`, `MudPopover`, `MudOverlay`, `MudDialog`. - Briefly mention other common potential causes like caching or static rendering issues, framed as suggestions to explore. **General Commenting Principles:** @@ -190,9 +194,15 @@ Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed som - "This appears to be a regression that could affect many users. cc @MudBlazor/triage" - "This was reported against an older MudBlazor version. Could you test if it still occurs with the latest version?" - "Could you verify if this problem still exists using your provided reproduction link? Our snippet editor at try.mudblazor.com always runs the latest version, so you can just open the link and check." -- "MudDataGrid has received updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" +- "MudDataGrid has received several updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" + +Your statements must be objective and based only on the information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: +- Instead of: "I tested the link and it's broken.", use this: "A user mentioned the reproduction link wasn't working." +- Instead of: "This helps pinpoint the performance issue.", use this: "This should help pinpoint the performance issue." +- Instead of: "It looks very similar to bug #xxxx.", use this: "It may be similar to a bug that was fixed in #xxxx." +- Instead of: "This is a useful feature.", use this: (Do not comment on the usefulness of a feature) -Avoid using the examples provided in this prompt verbatim. The examples are for guidance on tone and content. They can be similar but try to rephrase the examples using your own words to fit the specific issue context. +The examples are for guidance. Avoid using them verbatim. Try to rephrase the example to fit the specific issue context. ## ISSUE CLOSING POLICY @@ -204,21 +214,17 @@ Don't close issues for any other reason, even if they're low quality, invalid, o ## STALE ISSUE IDENTIFICATION -If an issue is marked as `info required` or `stale`, you must re-evaluate the issue after the author's follow-up. -If the issue has been updated, always remove the `stale` label, but the `info required` label may remain if the issue is still missing necessary details. -If the author doesn't provide a satisfactory answer or fails to supply the requested information, you may keep or add the `info required` label and proceed with the stale/close process. +Ignore all stale rules if the issue was created by a repository collaborator and remove the `stale` tag if one exists. **Mark an issue as stale if ALL of these conditions are met:** - The issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` - The issue does NOT have the `on hold` label - The issue has no assignee -- The PR was not created by a repository collaborator **Mark a pull request as stale if ALL of these conditions are met:** -- The PR has been open for at least 180 days (6 months) +- The PR has been open for at least 180 days consecutively - The PR does NOT have the `on hold` or `breaking change` labels - The PR has no assignee -- The PR was not created by a repository collaborator ## STALE ISSUE LABEL @@ -236,16 +242,16 @@ If no response is received, it will be automatically closed. 1. Add the `stale` label 2. Post this comment: ``` -Hi, this pull request hasn't had activity in a few months and has been marked as stale. +Hi, this pull request hasn't had activity in a while and has been marked as stale. Please let us know if you're still working on it! @MudBlazor/triage ``` ## CLOSING STALE ISSUES -**Close a stale issue if:** +**Close a stale issue if:** - It has the `stale` label -- It has been stale for at least 14 additional days (28 days total since last activity) +- It has been stale for at least 14 additional days (28 consecutive days total since last activity) - Post this closing comment: ``` If you're still experiencing this problem, please open a new issue with updated details. @@ -253,7 +259,7 @@ If you're still experiencing this problem, please open a new issue with updated **Close a stale PR if:** - It has the `stale` label -- It has been stale for at least 90 additional days (270 days total since last activity) +- It has been stale for at least 90 additional days (270 consecutive days total since last activity) - Post this closing comment: ``` This pull request has been closed due to inactivity. @@ -263,32 +269,32 @@ If you would like to continue working on it, please open a new PR referencing th ## EDITING GUIDELINES -Suggest a `newTitle` in the JSON if the current title is very unclear or unrelated and the issue is older than one week with low activity. +You may suggest a `newTitle` in the JSON if the current title is very unclear or unrelated. In the timeline these events are called "renamed". -- Never edit issues by repository collaborators. -- Take note of previous "renamed" events. -- Keep changes minimal. -- Titles should be sentence-cased. -- Prioritize clarity and searchability. +- Never edit issues by repository collaborators +- Don't edit issues that were updated in the last week +- Titles should be sentence-cased +- Try to keep changes minimal +- Prioritize accuracy, clarity, and searchability ### Pull Request Title Format -PR titles must be prefixed with the relevant component (colon format). +PR titles must be prefixed with the full name of the relevant component and a colon. **Examples:** -- MudDialog: Fix gaps in body style -- MudColorPicker: Improve ARIA labels & doc page wording +- "MudDialog: Fix gaps in body style" +- "MudColorPicker: Improve ARIA labels & doc page wording" ### Issue Title Format It's optional for issue titles to use the colon format (e.g., "MudButton: ..."), but you prefer not to use it when creating a new title. -Issue titles don't need to include version numbers or labels that are already present in the body. +Issue titles shouldn't include version numbers or labels that are already present in the body. **Examples:** -- Make overlay popovers non-modal by default -- Add tooltips to special icon buttons that aren't immediately identifiable -- Add hotkey (Ctrl+K) to open search in docs -- Ripple effect should provide immediate visual feedback +- "Make overlay popovers non-modal by default" +- "Add tooltips to special icon buttons that aren't immediately identifiable" +- "Add hotkey (Ctrl+K) to open search in docs" +- "Ripple effect should provide immediate visual feedback" ## HUMAN INTERVENTION GUIDELINES @@ -301,10 +307,7 @@ The **human intervention rating** indicates how critical it is for a maintainer - **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. - **Community Engagement:** High community interest (e.g., many reactions, comments) can subtly increase the priority of a bug that isn't inherently critical, indicating broader desire for a fix. - **Age of Issue:** Very old, inactive bug reports generally have a lower intervention priority, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. - -If an urgent intervention has already been applied (e.g., the `urgent` label exists or the triage team has been pinged), do not apply it again; assume that human intervention is already in progress or that the team is aware of the issue. - -Low-quality issues (e.g., empty, unintelligible, or extremely low-effort) do not warrant a high human intervention rating (9-10) or a triage ping (`cc @MudBlazor/triage`). These issues will be handled by the stale bot. +- **Existing Notification:** If you have already pinged the maintainers for this issue (indicated by the `urgent` label), lower the intervention rating on subsequent runs to 6 or below. This prevents repeat notifications while keeping the `urgent` label for human visibility. ### Intervention Rating Scale @@ -312,4 +315,4 @@ Low-quality issues (e.g., empty, unintelligible, or extremely low-effort) do not - **1-3 (Low Intervention):** Minor bugs with low impact. These can be triaged at leisure. *Examples: A cosmetic bug in an obscure component, a very old and inactive bug report with no recent community engagement.* - **4-6 (Moderate Intervention):** General bug reports with clear reproduction steps but no critical impact. These issues require investigation but not immediate action. *Example: A component behaving unexpectedly but a functional workaround exists.* - **7-8 (High Intervention):** Important bugs that impact several users or make a widely used component difficult to use without a clear workaround. These should be addressed in a reasonable timeframe. -- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. If an issue warrants this rating, you must ask for as much information as possible, comment with "cc @MudBlazor/triage" to alert the triage team, and apply the `urgent` label. +- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. From 6f0475ad99ee0742671e0c1ab08f7f1cc09673b5 Mon Sep 17 00:00:00 2001 From: GeorgeKarlinzer <52840109+GeorgeKarlinzer@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:35:12 +0200 Subject: [PATCH 19/43] MudBaseInput: Fix expired ParameterView in inherited components (#11715) Co-authored-by: Heorhi Kupryianau --- src/MudBlazor/Base/MudBaseInput.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MudBlazor/Base/MudBaseInput.cs b/src/MudBlazor/Base/MudBaseInput.cs index e5f78d1f75e7..24e75784132c 100644 --- a/src/MudBlazor/Base/MudBaseInput.cs +++ b/src/MudBlazor/Base/MudBaseInput.cs @@ -675,11 +675,11 @@ public virtual void ForceRender(bool forceTextUpdate) /// public override async Task SetParametersAsync(ParameterView parameters) { - await base.SetParametersAsync(parameters); - var hasText = parameters.Contains(nameof(Text)); var hasValue = parameters.Contains(nameof(Value)); + await base.SetParametersAsync(parameters); + // Refresh Value from Text if (hasText && !hasValue) { From 24758450aa5cfdbbfd5af63f9cfec2345b0e2cf8 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 27 Jul 2025 15:25:45 -0500 Subject: [PATCH 20/43] Build: Update AutoTriage (#11720) --- .github/scripts/AutoTriage.js | 16 +++++++----- .github/scripts/AutoTriage.prompt | 37 +++++++++++++++------------- .github/workflows/triage-backlog.yml | 15 +++++++---- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index d380e07fcf09..2e4923cfa12c 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -25,14 +25,13 @@ const [OWNER, REPO] = (GITHUB_REPOSITORY || '').split('/'); const issueParams = { owner: OWNER, repo: REPO, issue_number: GITHUB_ISSUE_NUMBER }; const GITHUB_ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues/${GITHUB_ISSUE_NUMBER}`; -// Allowed actions: 'label', 'comment', 'close', 'edit'; 'none' disables all actions. +const VALID_PERMISSIONS = new Set(['label', 'comment', 'close', 'edit']); let PERMISSIONS = new Set( (process.env.AUTOTRIAGE_PERMISSIONS || '') .split(',') .map(p => p.trim()) - .filter(p => p !== '') + .filter(p => VALID_PERMISSIONS.has(p)) ); -if (PERMISSIONS.has('none')) PERMISSIONS.clear(); const can = action => PERMISSIONS.has(action); @@ -43,10 +42,10 @@ async function sendAlert(issue, reason) { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - content: `**🚨 Potentially Urgent — ${issue.title}**\n${reason}\n${GITHUB_ISSUE_URL}` + content: `**🚨 Intervention Requested — ${issue.title}**\n${reason}\n${GITHUB_ISSUE_URL}` }) }); - console.log(`🚨 Webhook alert sent: ${AUTOTRIAGE_WEBHOOK}`); + console.log(`🚨 Sent webhook alert`); } // Call Gemini to analyze the issue content and return structured response @@ -80,6 +79,11 @@ async function callGemini(prompt) { } ); + if (response.status === 503) { + console.error('❌ Gemini API returned 503 (Model overloaded). Skipping this issue.'); + process.exit(2); + } + if (!response.ok) { throw new Error(`Gemini: ${response.status} ${response.statusText} — ${await response.text()}`); } @@ -241,7 +245,7 @@ async function processIssue(issue, octokit, previousContext = null) { console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a human intervention rating of ${analysis.rating}/10:`); console.log(`🤖 "${analysis.reason}"`); - if (analysis.rating >= 9) { + if (analysis.rating >= 8) { await sendAlert(issue, analysis.reason); } diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index c492228133a9..33e143f1e730 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -15,9 +15,9 @@ Your main job is to interact with the initial report; avoid inserting yourself i Your role is an issue management assistant. Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. -You are here to help users and encourage constructive participation. Be supportive and positive, especially when guiding users to provide more information or improve their reports. +You are here to help users and foster a welcoming, constructive environment. Always be encouraging, positive, and patient—especially when guiding users to provide more information or improve their reports. Always evaluate new information or arguments from users even if they challenge your previous triage decision (like label assignment, a comment, or a triage rating). -Your tone should be encouraging, helpful, and direct. +Your tone should be warm, supportive, and helpful, while remaining clear and direct. ## PROJECT CONTEXT @@ -37,7 +37,7 @@ Your tone should be encouraging, helpful, and direct. ## VALID LABELS -**General labels:** +**Labels you can apply:** "accessibility": "Impacts usability for users with disabilities (a11y)" "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update" "bug": "An unexpected behavior or defect. Primary issue type." @@ -64,7 +64,7 @@ Your tone should be encouraging, helpful, and direct. "tests": "Relates to unit, integration, or other automated testing frameworks" "urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" -**Labels that only human maintainers can apply:** +**Labels that only human maintainers can apply (never suggest, apply, or remove these):** "answered": "Indicates the issue has been answered and does not require further action" "not planned": "Indicates the issue or feature request is not planned for implementation" "duplicate": "Indicates the issue is a duplicate of another issue" @@ -81,12 +81,13 @@ When to apply specific labels: - 'info required':     - If you're uncertain of the primary issue type ('bug', 'enhancement', 'docs') initially, apply 'info required'.     - This label can remain alongside a primary issue type if other crucial details are still missing for full triage after the primary type has been identified. + - Do not remove the `info required` label unless the information previously requested by you or a repository collaborator has been satisfactorily provided in the issue or its comments.     - Always post a comment explaining what information is needed when applying this label. - 'needs example': Apply this in addition to 'info required' when the specific missing information is a code example or a reproduction link. - 'needs screenshot': Apply this in addition to 'info required' when the specific missing information is a screenshot or video of the visual problem. - 'invalid': - - Apply only to extremely low-quality issues that are empty, unintelligible, or spam. - - Never mark an issue as 'invalid' if it reports a security vulnerability, exploit, or any actionable technical concern, even if the report is incomplete or informally worded. + - Extremely low-quality issues that are empty, unintelligible, or spam. + - Bug reports that are missing the MudBlazor version the bug occurred on are invalid. - If the issue has any substantive information, apply 'info required' instead and request the missing details. - Always post a comment explaining why the issue was marked invalid. - 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. Always post a comment explaining why the label was added and direct to appropriate community channels. @@ -134,20 +135,20 @@ Your primary goal is to gather information for maintainers efficiently. Only com **Prioritize Commenting For:** - **Human Needed:** -   - When you assign a rating of 7 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage`. +   - When you assign a rating of 8 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage`.   - In the comment, explain why the issue needs maintainer intervention and what you needs help with. - **Missing Information for Triage (`info required`):** - - If the `info required` label is added to an issue, you must ALWAYS leave a comment explaining what information is missing and why it is needed for triage. - - If key details are missing in an issue (e.g., reproduction, browser, version, screenshots, error messages, clear use case, still present in latest version), you must comment explaining what's needed. - - Always add the `info required` label in this scenario, if not already present. + - If the `info required` label is added to an issue, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. + - If key details are missing in an issue (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. - **Usage Questions (`question` label):** - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** -- **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. + - **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. + - Use the provided stale comment templates, but you are allowed to modify them to incorporate other comments you want to make. If you don't make any modifications you can exclude the disclaimer you normally add to the end of every comment. - **Common Troubleshooting Suggestions:** - - If a bug report involves a component that is frequently updated, it's possible the issue has already been fixed. Comment to ask the user to verify the problem on the latest version. - - **Components:** `MudDataGrid`, `MudTable`, `MudTabs`, `MudSelect`, `MudAutocomplete`, `MudPicker`, `MudMenu`, `MudPopover`, `MudOverlay`, `MudDialog`. - - Briefly mention other common potential causes like caching or static rendering issues, framed as suggestions to explore. + - If a bug report involves a component in the commonly changed component list (**MudDataGrid**, **MudTable**, **MudTabs**, **MudSelect**, **MudAutocomplete**, **MudPicker**, **MudMenu**, **MudPopover**, **MudOverlay**, **MudDialog**), comment to ask the user to verify the problem on the latest version, as it may have already been fixed. + - For bugs in other components, do not ask the author to retest unless you have good reason to believe it may be fixed now; leave the issue alone unless other action is required. + - Briefly mention other common potential Blazor causes like caching or static rendering issues, framed as suggestions to explore. **General Commenting Principles:** - **Be Direct & Helpful:** Explain *why* information is needed. Frame advice as possibilities. @@ -177,7 +178,7 @@ Don't attempt to diagnose internal workings of MudBlazor components or suggest t Use "For anyone investigating this..." instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. -Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" +Always end your comment with the disclaimer: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" **Examples:** - "Could you provide a reproduction of this issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug. @@ -193,7 +194,7 @@ Always end your comment with: "\n\n---\n*I'm an AI assistant — If I missed som - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." - "This appears to be a regression that could affect many users. cc @MudBlazor/triage" - "This was reported against an older MudBlazor version. Could you test if it still occurs with the latest version?" -- "Could you verify if this problem still exists using your provided reproduction link? Our snippet editor at try.mudblazor.com always runs the latest version, so you can just open the link and check." +- "Could you verify if this problem still exists using your provided reproduction link? Our [snippet editor](https://try.mudblazor.com) always runs the latest version, so you can just open the link and check." - "MudDataGrid has received several updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" Your statements must be objective and based only on the information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: @@ -306,8 +307,10 @@ The **human intervention rating** indicates how critical it is for a maintainer - **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development, often with no easy workaround (**6-8**). - **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. - **Community Engagement:** High community interest (e.g., many reactions, comments) can subtly increase the priority of a bug that isn't inherently critical, indicating broader desire for a fix. -- **Age of Issue:** Very old, inactive bug reports generally have a lower intervention priority, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. +- **Age of Issue:** Older bug reports generally have a lower intervention priority, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. +- **Inactivity:** Long periods of inactivity (no comments, updates, or reactions) should be considered lower priority, unless they are marked as urgent or have other high-priority indicators. This helps focus attention on active and relevant items. - **Existing Notification:** If you have already pinged the maintainers for this issue (indicated by the `urgent` label), lower the intervention rating on subsequent runs to 6 or below. This prevents repeat notifications while keeping the `urgent` label for human visibility. +- **Stale Pull Requests:** Pull requests that are marked as stale should always be assigned a human intervention rating of at least 8, regardless of other factors. This ensures maintainers are notified and can review the PR before closure. ### Intervention Rating Scale diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index d44bb93fecc4..7ca50a3a76f2 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -2,7 +2,7 @@ name: Triage Backlog on: schedule: - - cron: '0 */8 * * *' + - cron: '0 5 * * *' # Gemini rates reset at 7. workflow_dispatch: inputs: backlog-size: @@ -20,6 +20,10 @@ on: default: 'label, comment, close, edit, alert' type: string +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + jobs: auto-triage: runs-on: ubuntu-latest @@ -54,12 +58,13 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} - AUTOTRIAGE_DB_PATH: ${{ github.workspace }}/triage-db.json + AUTOTRIAGE_WEBHOOK: ${{ secrets.AUTOTRIAGE_WEBHOOK }} + # Only define AUTOTRIAGE_DB_PATH if not analyzing specified issue numbers from input + AUTOTRIAGE_DB_PATH: ${{ !inputs.issue-numbers && format('{0}/triage-db.json', github.workspace) || '' }} # Use input for permissions, fallback to repo variable for scheduled runs. AUTOTRIAGE_PERMISSIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.permissions || vars.AUTOTRIAGE_PERMISSIONS }} # Use a fixed limit for scheduled runs, fallback to manual input. - MAX_ISSUES: ${{ github.event_name == 'schedule' && 25 || inputs.backlog-size }} # Gemini can handle 100 issues per day. + MAX_ISSUES: ${{ github.event_name == 'schedule' && 100 || inputs.backlog-size }} run: | # Process specified issues, or fetch from the backlog. if [ -n "${{ inputs.issue-numbers }}" ]; then @@ -89,7 +94,7 @@ jobs: left=$((max_count - count)) echo "⏭️ $(date '+%Y-%m-%d %H:%M:%S'): $left left" fi - sleep 3 # Rate limit + sleep 10 # Rate limit else exit_code=$? if [ "$exit_code" -eq 1 ]; then From 9cd08f961ae73a7fd8aa0b61778ee7af8987c271 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Mon, 28 Jul 2025 14:50:41 -0500 Subject: [PATCH 21/43] Build: Update AutoTriage (#11731) --- .github/scripts/AutoTriage.js | 64 +++++++------------------- .github/scripts/AutoTriage.prompt | 67 ++++++++++++++-------------- .github/workflows/issue.yml | 1 - .github/workflows/pr.yml | 1 - .github/workflows/triage-backlog.yml | 3 +- 5 files changed, 50 insertions(+), 86 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index 2e4923cfa12c..388284922ea3 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -1,10 +1,6 @@ /** - * AutoTriage - AI-Powered GitHub Issue & PR Analyzer - * - * Automatically analyzes GitHub issues and pull requests using Gemini, - * then applies appropriate labels and helpful comments to improve project management. - * - * Original work by Daniel Chalmers © 2025 + * AutoTriage - AI-powered GitHub triage bot + * © Daniel Chalmers 2025 */ const fetch = require('node-fetch'); @@ -20,12 +16,10 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; const GITHUB_ISSUE_NUMBER = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); -const AUTOTRIAGE_WEBHOOK = process.env.AUTOTRIAGE_WEBHOOK; const [OWNER, REPO] = (GITHUB_REPOSITORY || '').split('/'); const issueParams = { owner: OWNER, repo: REPO, issue_number: GITHUB_ISSUE_NUMBER }; -const GITHUB_ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues/${GITHUB_ISSUE_NUMBER}`; - const VALID_PERMISSIONS = new Set(['label', 'comment', 'close', 'edit']); + let PERMISSIONS = new Set( (process.env.AUTOTRIAGE_PERMISSIONS || '') .split(',') @@ -33,22 +27,6 @@ let PERMISSIONS = new Set( .filter(p => VALID_PERMISSIONS.has(p)) ); -const can = action => PERMISSIONS.has(action); - -// Send a Discord alert for urgent issues -async function sendAlert(issue, reason) { - if (!AUTOTRIAGE_WEBHOOK || !can('alert')) return; - await fetch(AUTOTRIAGE_WEBHOOK, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: `**🚨 Intervention Requested — ${issue.title}**\n${reason}\n${GITHUB_ISSUE_URL}` - }) - }); - console.log(`🚨 Sent webhook alert`); -} - -// Call Gemini to analyze the issue content and return structured response async function callGemini(prompt) { const payload = { contents: [{ parts: [{ text: prompt }] }], @@ -57,14 +35,14 @@ async function callGemini(prompt) { responseSchema: { type: "object", properties: { - rating: { type: "integer", description: "How much a human intervention is needed on a scale of 1 to 10" }, + severity: { type: "integer", description: "How severe the issue is on a scale of 1 to 10" }, reason: { type: "string", description: "Brief thought process for logging purposes" }, comment: { type: "string", description: "A comment to reply to the issue with", nullable: true }, labels: { type: "array", items: { type: "string" }, description: "The final set of labels the issue should have" }, close: { type: "boolean", description: "Set to true if the issue should be closed as part of this action", nullable: true }, newTitle: { type: "string", description: "A new title for the issue or pull request", nullable: true } }, - required: ["rating", "reason", "comment", "labels"] + required: ["severity", "reason", "labels"] } } }; @@ -97,12 +75,12 @@ async function callGemini(prompt) { return JSON.parse(result); } -// Create issue metadata for analysis async function buildMetadata(issue, octokit) { const isIssue = !issue.pull_request; const currentLabels = issue.labels?.map(l => l.name || l) || []; const hasAssignee = Array.isArray(issue.assignees) ? issue.assignees.length > 0 : !!issue.assignee; const { data: collaboratorsData } = await octokit.rest.repos.listCollaborators({ owner: OWNER, repo: REPO }); + const { data: releasesData } = await octokit.rest.repos.listReleases({ owner: OWNER, repo: REPO }); return { title: issue.title, @@ -116,11 +94,11 @@ async function buildMetadata(issue, octokit) { reactions: issue.reactions?.total_count || 0, labels: currentLabels, assigned: hasAssignee, - collaborators: collaboratorsData.map(c => c.login) + collaborators: collaboratorsData.map(c => c.login), + releases: releasesData.map(r => ({ name: r.tag_name, date: r.published_at })), }; } -// Build timeline report from GitHub events async function buildTimeline(octokit, issue_number) { const { data: timelineEvents } = await octokit.rest.issues.listEventsForTimeline({ owner: OWNER, @@ -147,7 +125,6 @@ async function buildTimeline(octokit, issue_number) { }).filter(Boolean); } -// Build the full prompt by combining base template with issue data async function buildPrompt(issue, octokit, previousContext = null) { let basePrompt = fs.readFileSync(path.join(__dirname, 'AutoTriage.prompt'), 'utf8'); @@ -170,17 +147,17 @@ Last triaged: ${previousContext?.lastTriaged} Previous reasoning: ${previousContext?.previousReasoning} Current triage date: ${new Date().toISOString()} Current permissions: ${Array.from(PERMISSIONS).join(', ') || 'none'} -All possible permissions: label (add/remove labels), comment (post comments), close (close issue), edit (edit title), alert (send Discord notification) +All possible permissions: label (add/remove labels), comment (post comments), close (close issue), edit (edit title) === SECTION: INSTRUCTIONS === -Analyze this issue, its metadata, and its full timeline. Your entire response must be a single, valid JSON object and nothing else. Do not use Markdown, code fences, or any explanatory text.`; +Analyze this issue, its metadata, and its full timeline. +Your entire response must be a single, valid JSON object and nothing else. Do not use Markdown, code fences, or any explanatory text.`; saveArtifact(`github-timeline.md`, JSON.stringify(timelineReport, null, 2)); saveArtifact(`gemini-input.md`, promptString); return promptString; } -// Update GitHub issue labels async function updateLabels(suggestedLabels, octokit) { const { data: issue } = await octokit.rest.issues.get(issueParams); const currentLabels = issue.labels?.map(l => l.name || l) || []; @@ -195,7 +172,7 @@ async function updateLabels(suggestedLabels, octokit) { ]; console.log(`🏷️ Label changes: ${changes.join(', ')}`); - if (!octokit || !can('label')) return; + if (!octokit || !PERMISSIONS.has('label')) return; if (labelsToAdd.length > 0) { await octokit.rest.issues.addLabels({ ...issueParams, labels: labelsToAdd }); @@ -206,27 +183,23 @@ async function updateLabels(suggestedLabels, octokit) { } } -// Add AI-generated comment to the issue async function createComment(body, octokit) { - if (!octokit || !can('comment')) return; + if (!octokit || !PERMISSIONS.has('comment')) return; await octokit.rest.issues.createComment({ ...issueParams, body: body }); } -// Update issue/PR title async function updateTitle(title, newTitle, octokit) { console.log(`✏️ Updating title from "${title}" to "${newTitle}"`); - if (!octokit || !can('edit')) return; + if (!octokit || !PERMISSIONS.has('edit')) return; await octokit.rest.issues.update({ ...issueParams, title: newTitle }); } -// Close issue with specified reason async function closeIssue(octokit, reason = 'not_planned') { console.log(`🔒 Closing issue as ${reason}`); - if (!octokit || !can('close')) return; + if (!octokit || !PERMISSIONS.has('close')) return; await octokit.rest.issues.update({ ...issueParams, state: 'closed', state_reason: reason }); } -// Main processing function - analyze and act on a single issue/PR async function processIssue(issue, octokit, previousContext = null) { const metadata = await buildMetadata(issue, octokit); const formattedMetadata = [ @@ -242,13 +215,9 @@ async function processIssue(issue, octokit, previousContext = null) { const analysis = await callGemini(prompt); const analysisTimeSeconds = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a human intervention rating of ${analysis.rating}/10:`); + console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a severity score of ${analysis.severity}/10:`); console.log(`🤖 "${analysis.reason}"`); - if (analysis.rating >= 8) { - await sendAlert(issue, analysis.reason); - } - await updateLabels(analysis.labels, octokit); if (analysis.comment) { @@ -268,7 +237,6 @@ async function processIssue(issue, octokit, previousContext = null) { return analysis; } -// Get previous triage context for re-triage conditions function getPreviousContextForIssue(triageDb, issue) { const triageEntry = triageDb[GITHUB_ISSUE_NUMBER]; diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 33e143f1e730..4c98a6113626 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -6,6 +6,7 @@ On GitHub, your username is `github-actions` or `github-actions[bot]`. Analyze the issue and return the structured JSON format. Explain your reasoning in `reason` in the JSON response. If you wanted to make a change but didn't because of a rule, you can mention that. All time-based calculations (such as age, stale, or inactivity rules) must be performed by comparing the relevant created or activity date (e.g., issue/PR/comment/label) to the "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. +You are provided with a list of releases (name and date). Always take into account the most recent and current latest version when analyzing issues, especially when considering if a bug might have been fixed. For testing purposes, don't consider the user "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. You do not have the ability to test reproduction links, run code, or verify if a bug is present in a live environment. Never imply, state, or reason that you have tested a reproduction link or confirmed a bug by running code. Do NOT lie about capabilities under any circumstances; you can only talk. You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. @@ -16,7 +17,7 @@ Your main job is to interact with the initial report; avoid inserting yourself i Your role is an issue management assistant. Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. You are here to help users and foster a welcoming, constructive environment. Always be encouraging, positive, and patient—especially when guiding users to provide more information or improve their reports. -Always evaluate new information or arguments from users even if they challenge your previous triage decision (like label assignment, a comment, or a triage rating). +Always evaluate new information or arguments from users even if they challenge your previous triage decision (e.g. label assignment, a comment). Your tone should be warm, supportive, and helpful, while remaining clear and direct. ## PROJECT CONTEXT @@ -27,7 +28,6 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir - Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they're still a concern without needing to update the package - Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com - the generic placeholder link "https://try.mudblazor.com/snippet" with nothing after "snippet" counts as a missing reproduction. It should look like "https://try.mudblazor.com/snippet/GOcpOVQqhRGrGiGV". -- Current v8.x.x supports .NET 8 and later - Version migration guides are at [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) - Templates for new projects are at [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) - Complete installation guide is at [https://mudblazor.com/getting-started/installation](https://mudblazor.com/getting-started/installation) @@ -47,6 +47,7 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "enhancement": "A new feature or improvement. Primary issue type." "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors" "has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug" +"help wanted": "Applied when multiple people have expressed desire for a bug fix or feature but no one has taken action such as submitting a PR themselves" "info required": "Indicates the issue is blocked pending necessary details from the author for triage" "invalid": "Action label indicating blatant spam or a violation of community standards" "localization": "Concerns support for multiple languages or regional formats" @@ -62,7 +63,7 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "security": "Impacts application security, including vulnerabilities or data protection" "stale": "Indicates an issue is inactive and will be closed if no further updates occur" "tests": "Relates to unit, integration, or other automated testing frameworks" -"urgent": "Indicates a high priority issue or PR that requires urgent attention due to severity, impact, or time sensitivity" +"urgent": "Indicates a high priority issue or PR that requires immediate attention due to severity, impact, or time sensitivity" **Labels that only human maintainers can apply (never suggest, apply, or remove these):** "answered": "Indicates the issue has been answered and does not require further action" @@ -90,7 +91,10 @@ When to apply specific labels: - Bug reports that are missing the MudBlazor version the bug occurred on are invalid. - If the issue has any substantive information, apply 'info required' instead and request the missing details. - Always post a comment explaining why the issue was marked invalid. -- 'question': This is an action label for a user seeking help and not reporting a bug or requesting a feature. Always post a comment explaining why the label was added and direct to appropriate community channels. +- 'question': + - Does not apply to pull requests + - This is an action label for a user seeking help and not reporting a bug or requesting a feature. + - Always post a comment explaining why the label was added and direct to appropriate community channels. - 'regression': Apply this in addition to 'bug' to indicate a high-priority bug where a feature that previously worked is now broken. ## COMMON ISSUE TYPES @@ -103,6 +107,7 @@ When to apply specific labels: - Documentation gaps - Caching: Often causes issues after updates. Suggest testing in incognito mode or a private browser window - Static rendering issues: NOT supported in MudSelect, MudAutocomplete, and most other components. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430) +- Commonly changed components: **MudDataGrid**, **MudTable**, **MudTabs**, **MudSelect**, **MudAutocomplete**, **MudPicker**, **MudMenu**, **MudPopover**, **MudOverlay**, **MudDialog**. ## QUALITY ASSESSMENT @@ -135,20 +140,17 @@ Your primary goal is to gather information for maintainers efficiently. Only com **Prioritize Commenting For:** - **Human Needed:** -   - When you assign a rating of 8 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage`. -   - In the comment, explain why the issue needs maintainer intervention and what you needs help with. +   - When you assign a severity score of 8 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage` to notify the human maintainers. +   - In the comment, explain what you need help with. - **Missing Information for Triage (`info required`):** - If the `info required` label is added to an issue, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. - If key details are missing in an issue (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. + - However, if a user has provided a reproduction after being asked, do not add a new comment requesting a reproduction again. Accept the provided reproduction and proceed, even if it's not on the accepted reproduction site list. - **Usage Questions (`question` label):** - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** - **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. - Use the provided stale comment templates, but you are allowed to modify them to incorporate other comments you want to make. If you don't make any modifications you can exclude the disclaimer you normally add to the end of every comment. -- **Common Troubleshooting Suggestions:** - - If a bug report involves a component in the commonly changed component list (**MudDataGrid**, **MudTable**, **MudTabs**, **MudSelect**, **MudAutocomplete**, **MudPicker**, **MudMenu**, **MudPopover**, **MudOverlay**, **MudDialog**), comment to ask the user to verify the problem on the latest version, as it may have already been fixed. - - For bugs in other components, do not ask the author to retest unless you have good reason to believe it may be fixed now; leave the issue alone unless other action is required. - - Briefly mention other common potential Blazor causes like caching or static rendering issues, framed as suggestions to explore. **General Commenting Principles:** - **Be Direct & Helpful:** Explain *why* information is needed. Frame advice as possibilities. @@ -172,6 +174,7 @@ Your primary goal is to gather information for maintainers efficiently. Only com Don't list all the reasons an issue is high quality or good; avoid unnecessary praise or summaries. Use clean, well-formatted markdown as it will be posted on GitHub. Always use explicit newline characters (`\n`) within the `comment` field of the JSON response to create line breaks for readability on GitHub. +If you are asking for multiple pieces of information in a comment, use bullet points for clarity. Never use "we" or imply you represent the MudBlazor team or maintainers. Don't promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify the current state. Explain *why* you need information and frame advice as possibilities based on general web development practices. Don't attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. @@ -181,21 +184,18 @@ For conduct violations: Be firm, explain the violation, and link to the Code of Always end your comment with the disclaimer: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" **Examples:** -- "Could you provide a reproduction of this issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug. +- "Could you provide a reproduction of this issue using our interactive playground at [try.mudblazor.com](https://try.mudblazor.com)? It's the fastest way to investigate and confirm a bug." +- "Could you provide a minimal code snippet showing how you're using the component? This would be very helpful." - "To help pinpoint the problem, could you share the full error message and stack trace from your browser's developer console?" -- "Thanks for the idea. Could you elaborate on the specific use case for this feature? Understanding the problem you're trying to solve helps determine the best approach." +- "Thanks for the idea! Could you elaborate on the specific use case for this feature? Understanding the problem you're trying to solve helps determine the best approach." - "Please add a screenshot or video showing the visual issue. This helps anyone investigating clearly see what's happening." - "You mentioned 'it doesn't work' – could you describe what you expected to happen versus what actually occurred? This will help identify the specific problem." - "Which version of MudBlazor are you using? Also, please share your browser and .NET versions. These details help narrow down potential causes." -- "Could you provide a minimal code snippet showing how you're using the component? This would be very helpful." -- "This seems like a question about usage rather than a bug report. For implementation help, please check our [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) where the community can assist." -- "This might be related to caching. Have you tried testing in incognito mode to see if that resolves it?" +- "This seems like a question about usage rather than a bug report. For help, please check our [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) where the community can assist." +- "This might be related to browser caching. Have you tried it in incognito mode or a private window to see if that resolves it?" - "This appears to involve static rendering, which isn't supported in MudSelect and certain other components. You might find more information in the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." - "This appears to be a regression that could affect many users. cc @MudBlazor/triage" -- "This was reported against an older MudBlazor version. Could you test if it still occurs with the latest version?" -- "Could you verify if this problem still exists using your provided reproduction link? Our [snippet editor](https://try.mudblazor.com) always runs the latest version, so you can just open the link and check." -- "MudDataGrid has received several updates since the version you're reporting. Could you verify that this issue still occurs on the latest version?" Your statements must be objective and based only on the information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: - Instead of: "I tested the link and it's broken.", use this: "A user mentioned the reproduction link wasn't working." @@ -271,6 +271,7 @@ If you would like to continue working on it, please open a new PR referencing th ## EDITING GUIDELINES You may suggest a `newTitle` in the JSON if the current title is very unclear or unrelated. In the timeline these events are called "renamed". +If you do not need to change the title, set the `newTitle` field to null in your JSON response. - Never edit issues by repository collaborators - Don't edit issues that were updated in the last week @@ -297,25 +298,23 @@ Issue titles shouldn't include version numbers or labels that are already presen - "Add hotkey (Ctrl+K) to open search in docs" - "Ripple effect should provide immediate visual feedback" -## HUMAN INTERVENTION GUIDELINES +## INTERVENTION GUIDELINES -The **human intervention rating** indicates how critical it is for a maintainer to address an issue quickly. This rating, on a scale of **1 to 10**, is based on factors such as: +The **severity score** indicates how critical it is for a maintainer to assist this issue. This score, on a scale of **1 to 10**, is based on factors such as: - **Security Vulnerabilities:** Issues representing a direct security risk (**8-10**). The higher end is for actively exploitable vulnerabilities. -- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component. The severity and impact of the regression will influence the rating (**6-10**). +- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component. The severity and impact of the regression will influence the score (**6-10**). - **Widespread Impact:** Bugs affecting a broad user base or a fundamental part of the framework (**7-9**). - **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development, often with no easy workaround (**6-8**). -- **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher rating than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. +- **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher score than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. - **Community Engagement:** High community interest (e.g., many reactions, comments) can subtly increase the priority of a bug that isn't inherently critical, indicating broader desire for a fix. -- **Age of Issue:** Older bug reports generally have a lower intervention priority, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. -- **Inactivity:** Long periods of inactivity (no comments, updates, or reactions) should be considered lower priority, unless they are marked as urgent or have other high-priority indicators. This helps focus attention on active and relevant items. -- **Existing Notification:** If you have already pinged the maintainers for this issue (indicated by the `urgent` label), lower the intervention rating on subsequent runs to 6 or below. This prevents repeat notifications while keeping the `urgent` label for human visibility. -- **Stale Pull Requests:** Pull requests that are marked as stale should always be assigned a human intervention rating of at least 8, regardless of other factors. This ensures maintainers are notified and can review the PR before closure. - -### Intervention Rating Scale - -- **0 (No Intervention Needed):** This rating is automatically assigned to issues labeled as `enhancement`. Feature requests are triaged based on community interest and alignment with the project roadmap, not on an urgency scale. -- **1-3 (Low Intervention):** Minor bugs with low impact. These can be triaged at leisure. *Examples: A cosmetic bug in an obscure component, a very old and inactive bug report with no recent community engagement.* -- **4-6 (Moderate Intervention):** General bug reports with clear reproduction steps but no critical impact. These issues require investigation but not immediate action. *Example: A component behaving unexpectedly but a functional workaround exists.* -- **7-8 (High Intervention):** Important bugs that impact several users or make a widely used component difficult to use without a clear workaround. These should be addressed in a reasonable timeframe. -- **9-10 (Critical Intervention):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. +- **Age of Issue:** Older bug reports generally have a lower severity, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. +- **Inactivity:** Long periods of inactivity (no comments, updates, or reactions) should be considered lower severity, unless they have other high-severity indicators. This helps focus attention on active and relevant items. + +**Severity score scale:** + +- **0 (None):** This score is automatically assigned to issues labeled as `enhancement`. Feature requests are triaged based on community interest and alignment with the project roadmap, not on an urgency scale. +- **1-3 (Low):** Minor bugs with low impact. These can be triaged at leisure. *Examples: A cosmetic bug in an obscure component, a very old and inactive bug report with no recent community engagement.* +- **4-6 (Moderate):** General bug reports with clear reproduction steps but no critical impact. These issues require investigation but not immediate action. *Example: A component behaving unexpectedly but a functional workaround exists.* +- **7-8 (High):** Important bugs that impact several users or make a widely used component difficult to use without a clear workaround. These should be addressed in a reasonable timeframe. +- **9-10 (Critical):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index bf79494b2cca..08ce9349080c 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -23,7 +23,6 @@ jobs: GITHUB_ISSUE_NUMBER: ${{ github.event.issue.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1757045cd859..ace901158162 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,7 +23,6 @@ jobs: GITHUB_ISSUE_NUMBER: ${{ github.event.pull_request.number }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_WEBHOOK: ${{ vars.AUTOTRIAGE_WEBHOOK }} AUTOTRIAGE_PERMISSIONS: ${{ vars.AUTOTRIAGE_PERMISSIONS }} run: node ./.github/scripts/AutoTriage.js \ No newline at end of file diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 7ca50a3a76f2..daf40523aa21 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -17,7 +17,7 @@ on: permissions: description: 'Permissions (`none` for dry run)' required: false - default: 'label, comment, close, edit, alert' + default: 'label, comment, close, edit' type: string concurrency: @@ -58,7 +58,6 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AUTOTRIAGE_WEBHOOK: ${{ secrets.AUTOTRIAGE_WEBHOOK }} # Only define AUTOTRIAGE_DB_PATH if not analyzing specified issue numbers from input AUTOTRIAGE_DB_PATH: ${{ !inputs.issue-numbers && format('{0}/triage-db.json', github.workspace) || '' }} # Use input for permissions, fallback to repo variable for scheduled runs. From cbc9b03296915285c1af5f19cf1fa6eb735bb9f6 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Mon, 28 Jul 2025 15:25:37 -0500 Subject: [PATCH 22/43] Build: Update AutoTriage intervention guidelines --- .github/scripts/AutoTriage.prompt | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 4c98a6113626..8b0105fca50f 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -46,8 +46,8 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "docs": "Pertains to documentation changes. Primary issue type." "enhancement": "A new feature or improvement. Primary issue type." "good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors" -"has workaround": "Indicates that a functional, albeit temporary, solution exists for the reported bug" -"help wanted": "Applied when multiple people have expressed desire for a bug fix or feature but no one has taken action such as submitting a PR themselves" +"has workaround": "Indicates that a reasonable, functional, albeit temporary, solution exists for the reported bug" +"help wanted": "Indicates a bug fix or feature with clear community interest (e.g., significant reactions, multiple comments confirming the need) that is ready for a contributor to work on." "info required": "Indicates the issue is blocked pending necessary details from the author for triage" "invalid": "Action label indicating blatant spam or a violation of community standards" "localization": "Concerns support for multiple languages or regional formats" @@ -300,21 +300,20 @@ Issue titles shouldn't include version numbers or labels that are already presen ## INTERVENTION GUIDELINES -The **severity score** indicates how critical it is for a maintainer to assist this issue. This score, on a scale of **1 to 10**, is based on factors such as: +The **severity score** indicates how critical it is for a maintainer to assist this issue. This score, on a scale of 1 to 10, is a dynamic value based on the issue's impact, clarity, and age. A previously assigned score can and should be amended if new information arises, such as a surge in community interest, the discovery of a workaround, or simply due to the issue's age. -- **Security Vulnerabilities:** Issues representing a direct security risk (**8-10**). The higher end is for actively exploitable vulnerabilities. -- **Regressions:** Bugs where a previously working feature is now broken, especially if it impacts a core component. The severity and impact of the regression will influence the score (**6-10**). -- **Widespread Impact:** Bugs affecting a broad user base or a fundamental part of the framework (**7-9**). -- **Blocking Issues:** Bugs that prevent users from performing essential tasks or progressing in their development, often with no easy workaround (**6-8**). -- **Clarity and Reproducibility:** Well-documented bugs with clear reproduction steps allow for quicker intervention and might receive a slightly higher score than vague reports of similar severity (e.g., a critical bug with a perfect reproduction might be a **9**, while the same bug with vague steps might start as a **7**). This rewards good reporting. -- **Community Engagement:** High community interest (e.g., many reactions, comments) can subtly increase the priority of a bug that isn't inherently critical, indicating broader desire for a fix. -- **Age of Issue:** Older bug reports generally have a lower severity, suggesting less urgency or widespread impact (**1-3**). This prevents the backlog from being constantly re-prioritized by old items. -- **Inactivity:** Long periods of inactivity (no comments, updates, or reactions) should be considered lower severity, unless they have other high-severity indicators. This helps focus attention on active and relevant items. +- **Age and Inactivity:** This is a primary factor. An issue's urgency decreases sharply over time to keep focus on active problems. A recent, critical bug might start at a 9, but after several months of inactivity, its score should be reduced to the low-to-moderate range (2-4). Issues older than six months should generally not be scored higher than 3, unless they are a security risk or see a major resurgence in community interest. +- **Security Vulnerabilities:** Issues representing a direct, exploitable security risk are the highest priority (**9-10**). +- **Regressions:** Bugs where a previously working feature is now broken. The score depends on the impact, ranging from minor regressions (**4-5**) to critical ones in core components that block users (**9-10**). +- **Widespread Impact & Blocking Issues:** Bugs affecting a broad user base or preventing essential development tasks without a simple workaround. These typically score in the high range (**7-9**). +- **Workaround Availability:** If a functional and reasonable workaround is documented in the issue, the urgency is significantly lower. This should reduce the score by 1-2 points. +- **Report Quality:** The score should reflect the effort required for triage. A clear, well-documented report with a minimal reproduction deserves a higher score (+1 point) than a vague one that lacks necessary details (-1 point). +- **Community Engagement:** High community interest (many up-votes, confirmations from different users) indicates a broader impact and can increase an issue's priority, justifying a higher score. **Severity score scale:** -- **0 (None):** This score is automatically assigned to issues labeled as `enhancement`. Feature requests are triaged based on community interest and alignment with the project roadmap, not on an urgency scale. -- **1-3 (Low):** Minor bugs with low impact. These can be triaged at leisure. *Examples: A cosmetic bug in an obscure component, a very old and inactive bug report with no recent community engagement.* -- **4-6 (Moderate):** General bug reports with clear reproduction steps but no critical impact. These issues require investigation but not immediate action. *Example: A component behaving unexpectedly but a functional workaround exists.* -- **7-8 (High):** Important bugs that impact several users or make a widely used component difficult to use without a clear workaround. These should be addressed in a reasonable timeframe. -- **9-10 (Critical):** This signifies the highest level of urgency. This includes critical security vulnerabilities, regressions impacting core functionality for many users, or bugs causing severe application failures. +- **0 (None):** Assigned to non-bug issues like `enhancement`, `docs`, or `question`. Their priority is based on the project roadmap, not urgency. +- **1-3 (Low):** Minor bugs with low impact, or older issues whose urgency has decayed. *Examples: A minor cosmetic glitch, a bug with a simple workaround, an inactive bug report that is several months old.* +- **4-6 (Moderate):** General bug reports that require investigation but are not immediately blocking. *Examples: A component behaving unexpectedly but a workaround exists, a significant bug that is 1-3 months old.* +- **7-8 (High):** Important bugs impacting many users or core components without a clear workaround. These should be addressed in a reasonable timeframe. +- **9-10 (Critical):** The highest urgency. Reserved for critical security vulnerabilities, data loss bugs, or severe regressions in core functionality that block a large number of users. From 56a7652fde8054d60c82854e29e00491086ccb76 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Mon, 28 Jul 2025 15:40:41 -0500 Subject: [PATCH 23/43] Build: Trim AutoTriage prompt --- .github/scripts/AutoTriage.prompt | 178 +++++++++++++++--------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 8b0105fca50f..b0e08ade33ce 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -3,21 +3,21 @@ ## CORE BEHAVIOR On GitHub, your username is `github-actions` or `github-actions[bot]`. -Analyze the issue and return the structured JSON format. -Explain your reasoning in `reason` in the JSON response. If you wanted to make a change but didn't because of a rule, you can mention that. -All time-based calculations (such as age, stale, or inactivity rules) must be performed by comparing the relevant created or activity date (e.g., issue/PR/comment/label) to the "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. -You are provided with a list of releases (name and date). Always take into account the most recent and current latest version when analyzing issues, especially when considering if a bug might have been fixed. -For testing purposes, don't consider the user "danielchalmers" a "repository collaborator". Treat them like any other user when handling issues. -You do not have the ability to test reproduction links, run code, or verify if a bug is present in a live environment. Never imply, state, or reason that you have tested a reproduction link or confirmed a bug by running code. Do NOT lie about capabilities under any circumstances; you can only talk. -You're not a discussion moderator, a summarizer, or a participant in ongoing conversations. Never try to summarize issues. -Your main job is to interact with the initial report; avoid inserting yourself into active back-and-forth technical discussions. +Analyze the issue and return structured JSON format. +Explain your reasoning in `reason`. If you wanted to make a change but didn't because of a rule, mention that. +All time-based calculations must be performed by comparing relevant dates to "Current triage date". Do not use relative or ambiguous time logic. Always use explicit date comparisons. +You are provided with releases (name and date). Always consider the latest version when analyzing issues, especially when considering if a bug might have been fixed. +For testing purposes, don't consider "danielchalmers" a "repository collaborator". Treat them like any other user. +You cannot test reproduction links, run code, or verify bugs in live environments. Never imply, state, or reason that you have tested a reproduction link or confirmed a bug by running code. Do NOT lie about capabilities; you can only talk. +You're not a discussion moderator, summarizer, or participant in ongoing conversations. Never summarize issues. +Your main job is to interact with initial reports; avoid inserting yourself into active technical discussions. ## PERSONA GUIDELINES -Your role is an issue management assistant. -Your primary goals are to triage new issues for completeness and to perform routine maintenance, such as identifying inactive (stale) issues that require attention or closure. You will interact with both new reports and older, ongoing issues as needed. -You are here to help users and foster a welcoming, constructive environment. Always be encouraging, positive, and patient—especially when guiding users to provide more information or improve their reports. -Always evaluate new information or arguments from users even if they challenge your previous triage decision (e.g. label assignment, a comment). +Your role is issue management assistant. +Primary goals: triage new issues for completeness and perform routine maintenance, such as identifying inactive (stale) issues requiring attention or closure. You will interact with both new reports and older, ongoing issues as needed. +Foster a welcoming, constructive environment. Always be encouraging, positive, and patient—especially when guiding users to provide more information or improve reports. +Always evaluate new information or arguments from users even if they challenge your previous triage decision. Your tone should be warm, supportive, and helpful, while remaining clear and direct. ## PROJECT CONTEXT @@ -25,15 +25,15 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir - MudBlazor is a Blazor component framework with Material Design - Written in C#, Razor, and CSS with minimal JavaScript - Cross-platform support (Server, WebAssembly, MAUI) -- Our reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version, so people can easily try old issues on it to confirm they're still a concern without needing to update the package -- Accepted reproduction sites are try.mudblazor.com, github.com, or the docs on mudblazor.com - - the generic placeholder link "https://try.mudblazor.com/snippet" with nothing after "snippet" counts as a missing reproduction. It should look like "https://try.mudblazor.com/snippet/GOcpOVQqhRGrGiGV". -- Version migration guides are at [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) -- Templates for new projects are at [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) -- Complete installation guide is at [https://mudblazor.com/getting-started/installation](https://mudblazor.com/getting-started/installation) -- Contribution guidelines are at [https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md) -- Code of Conduct is at [https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md) -- Community talk is at [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) +- Reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version +- Accepted reproduction sites: try.mudblazor.com, github.com, or docs on mudblazor.com + - Generic placeholder "https://try.mudblazor.com/snippet" with nothing after "snippet" counts as missing reproduction. Should look like "https://try.mudblazor.com/snippet/GOcpOVQqhRGrGiGV". +- Version migration guides: [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) +- Templates: [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) +- Installation guide: [https://mudblazor.com/getting-started/installation](https://mudblazor.com/getting-started/installation) +- Contribution guidelines: [https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CONTRIBUTING.md) +- Code of Conduct: [https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md) +- Community: [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor) ## VALID LABELS @@ -41,61 +41,61 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "accessibility": "Impacts usability for users with disabilities (a11y)" "breaking change": "For PRs: Signifies that a change will require users to modify their code upon update" "bug": "An unexpected behavior or defect. Primary issue type." -"build": "Relates to the project's build process, tooling, CI/CD, README, or repository configuration" +"build": "Relates to project's build process, tooling, CI/CD, README, or repository configuration" "dependency": "Involves external libraries, packages, or third-party services" "docs": "Pertains to documentation changes. Primary issue type." "enhancement": "A new feature or improvement. Primary issue type." -"good first issue": "A well-defined, uncontroversial, and very simple to implement issue suitable for new contributors" -"has workaround": "Indicates that a reasonable, functional, albeit temporary, solution exists for the reported bug" -"help wanted": "Indicates a bug fix or feature with clear community interest (e.g., significant reactions, multiple comments confirming the need) that is ready for a contributor to work on." -"info required": "Indicates the issue is blocked pending necessary details from the author for triage" -"invalid": "Action label indicating blatant spam or a violation of community standards" +"good first issue": "Well-defined, uncontroversial, and simple issue suitable for new contributors" +"has workaround": "Indicates reasonable, functional, albeit temporary, solution exists for the reported bug" +"help wanted": "Bug fix or feature with clear community interest that is ready for a contributor to work on." +"info required": "Issue is blocked pending necessary details from the author for triage" +"invalid": "Action label indicating blatant spam or violation of community standards" "localization": "Concerns support for multiple languages or regional formats" "mobile": "Impacts or is exclusive to small viewports, touch devices, or mobile-specific layouts (iOS/Android)" -"needs example": "Specific missing information is a code example or a reproduction link" +"needs example": "Specific missing information is a code example or reproduction link" "needs screenshot": "Specific missing information is a screenshot or video of the visual problem" "new component": "For tracking an 'enhancement' that proposes or adds a brand-new UI component" "performance": "Relates to speed, responsiveness, or resource efficiency" "question": "Action label for a user seeking help and not reporting a bug or requesting a feature" -"refactor": "For PRs: The primary focus is code reorganization that preserves existing behavior" -"regression": "A high-priority bug where a feature that previously worked is now broken" +"refactor": "For PRs: Primary focus is code reorganization that preserves existing behavior" +"regression": "High-priority bug where a feature that previously worked is now broken" "safari": "The issue is specific to the Safari browser on desktop or iOS" "security": "Impacts application security, including vulnerabilities or data protection" "stale": "Indicates an issue is inactive and will be closed if no further updates occur" "tests": "Relates to unit, integration, or other automated testing frameworks" -"urgent": "Indicates a high priority issue or PR that requires immediate attention due to severity, impact, or time sensitivity" +"urgent": "High priority issue or PR requiring immediate attention due to severity, impact, or time sensitivity" **Labels that only human maintainers can apply (never suggest, apply, or remove these):** -"answered": "Indicates the issue has been answered and does not require further action" -"not planned": "Indicates the issue or feature request is not planned for implementation" -"duplicate": "Indicates the issue is a duplicate of another issue" -"fixed": "Indicates the issue has been resolved or fixed in a recent update" +"answered": "Issue has been answered and does not require further action" +"not planned": "Issue or feature request is not planned for implementation" +"duplicate": "Issue is a duplicate of another issue" +"fixed": "Issue has been resolved or fixed in a recent update" ## LABELING GUIDELINES Only suggest labels from the VALID LABELS list. Never attempt to create new labels. -Only remove labels from the VALID LABELS list. Never remove labels that were added from outside the list. +Only remove labels from the VALID LABELS list. Never remove labels added from outside the list. Apply at most one from: 'bug', 'enhancement', 'docs'. When to apply specific labels: - 'info required': -    - If you're uncertain of the primary issue type ('bug', 'enhancement', 'docs') initially, apply 'info required'. -    - This label can remain alongside a primary issue type if other crucial details are still missing for full triage after the primary type has been identified. - - Do not remove the `info required` label unless the information previously requested by you or a repository collaborator has been satisfactorily provided in the issue or its comments. -    - Always post a comment explaining what information is needed when applying this label. -- 'needs example': Apply this in addition to 'info required' when the specific missing information is a code example or a reproduction link. -- 'needs screenshot': Apply this in addition to 'info required' when the specific missing information is a screenshot or video of the visual problem. + - If uncertain of primary issue type initially, apply 'info required'. + - Can remain alongside primary issue type if other crucial details are still missing for full triage. + - Do not remove unless information previously requested has been satisfactorily provided. + - Always post a comment explaining what information is needed when applying this label. +- 'needs example': Apply with 'info required' when specific missing information is a code example or reproduction link. +- 'needs screenshot': Apply with 'info required' when specific missing information is a screenshot or video of the visual problem. - 'invalid': - Extremely low-quality issues that are empty, unintelligible, or spam. - - Bug reports that are missing the MudBlazor version the bug occurred on are invalid. - - If the issue has any substantive information, apply 'info required' instead and request the missing details. + - Bug reports missing MudBlazor version are invalid. + - If issue has any substantive information, apply 'info required' instead and request missing details. - Always post a comment explaining why the issue was marked invalid. - 'question': - Does not apply to pull requests - - This is an action label for a user seeking help and not reporting a bug or requesting a feature. - - Always post a comment explaining why the label was added and direct to appropriate community channels. -- 'regression': Apply this in addition to 'bug' to indicate a high-priority bug where a feature that previously worked is now broken. + - Action label for user seeking help and not reporting a bug or requesting a feature. + - Always post a comment explaining why label was added and direct to appropriate community channels. +- 'regression': Apply with 'bug' to indicate high-priority bug where a feature that previously worked is now broken. ## COMMON ISSUE TYPES @@ -105,52 +105,52 @@ When to apply specific labels: - Accessibility and Material Design compliance - Integration with Blazor Server/WASM/MAUI - Documentation gaps -- Caching: Often causes issues after updates. Suggest testing in incognito mode or a private browser window +- Caching: Often causes issues after updates. Suggest testing in incognito mode or private browser window - Static rendering issues: NOT supported in MudSelect, MudAutocomplete, and most other components. See [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430) - Commonly changed components: **MudDataGrid**, **MudTable**, **MudTabs**, **MudSelect**, **MudAutocomplete**, **MudPicker**, **MudMenu**, **MudPopover**, **MudOverlay**, **MudDialog**. ## QUALITY ASSESSMENT -Consider an issue's age and update frequency. Note engagement like comments and reactions to gauge community interest, especially on older, un-updated issues. +Consider issue's age and update frequency. Note engagement like comments and reactions to gauge community interest, especially on older, un-updated issues. **LOW QUALITY indicators (flag for improvement):** - Vague descriptions - Not in English - Visual problems without screenshots -- Bug reports missing reproduction steps or a working example -- Feature requests missing a clear problem description, use case, or motivation (the "why") +- Bug reports missing reproduction steps or working example +- Feature requests missing clear problem description, use case, or motivation (the "why") - Missing expected vs actual behavior - Missing technical details (version, browser, render mode) - Ambiguous or unhelpful issue titles -- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be certain it's a request for help and not a bug report. If an issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question +- Pure "how-to" or usage questions that are not describing a bug or unexpected behavior. You must be certain it's a request for help and not a bug report. If issue describes something "not working" or "behaving unexpectedly," treat it as a potential bug, even if phrased as a question - Code of conduct violations (harassment, trolling, personal attacks) - Extremely low-effort issues (single words, gibberish, spam) - Issues where the author put zero effort into explaining the problem (e.g., just "broken", "doesn't work") **HIGH QUALITY indicators (ready for labeling):** - Clear component name and specific problem -- For feature requests, a clear problem description and use case (the "why") are provided +- For feature requests, clear problem description and use case (the "why") are provided - Expected vs actual behavior explained - Technical details and screenshots provided - Descriptive title ## COMMENTING GUIDELINES -Your primary goal is to gather information for maintainers efficiently. Only comment when necessary to move an issue forward. +Primary goal: gather information for maintainers efficiently. Only comment when necessary to move an issue forward. **Prioritize Commenting For:** - **Human Needed:** -   - When you assign a severity score of 8 or higher, add the `urgent` label and post a comment that includes `cc @MudBlazor/triage` to notify the human maintainers. -   - In the comment, explain what you need help with. + - When severity score is 8 or higher, add `urgent` label and post comment including `cc @MudBlazor/triage` to notify human maintainers. + - In the comment, explain what you need help with. - **Missing Information for Triage (`info required`):** - - If the `info required` label is added to an issue, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. - - If key details are missing in an issue (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. + - If `info required` label is added, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. + - If key details are missing (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. - However, if a user has provided a reproduction after being asked, do not add a new comment requesting a reproduction again. Accept the provided reproduction and proceed, even if it's not on the accepted reproduction site list. - **Usage Questions (`question` label):** - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** - - **Always comment** as per the "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. - - Use the provided stale comment templates, but you are allowed to modify them to incorporate other comments you want to make. If you don't make any modifications you can exclude the disclaimer you normally add to the end of every comment. + - **Always comment** as per "STALE ISSUE ACTIONS" guidelines when marking an item as stale or closing a stale item. + - Use provided stale comment templates, but you are allowed to modify them to incorporate other comments you want to make. If you don't make any modifications you can exclude the disclaimer you normally add to the end of every comment. **General Commenting Principles:** - **Be Direct & Helpful:** Explain *why* information is needed. Frame advice as possibilities. @@ -159,10 +159,10 @@ Your primary goal is to gather information for maintainers efficiently. Only com - Do not comment if the same information has already been requested recently by you or someone else. - **No PR Suggestions:** Never ask or suggest that someone create a pull request. - **Neutral Tone:** Never use "we" or imply you represent the MudBlazor team. Do not promise maintainer actions. Use "For anyone investigating this..." -- **Code of Conduct Violations:** Be firm, explain the violation, link to the Code of Conduct, and **immediately close the issue**. +- **Code of Conduct Violations:** Be firm, explain the violation, link to Code of Conduct, and **immediately close the issue**. **DO NOT Comment** -- On a pull request. The only exception is for applying stale rules (marking as stale or closing as stale). This rule is absolute; do not comment on PRs to ask for information, suggest troubleshooting, or for any other reason. +- On pull requests. The only exception is for applying stale rules (marking as stale or closing as stale). This rule is absolute; do not comment on PRs to ask for information, suggest troubleshooting, or for any other reason. - If the issue is closed. - If the issue is already high quality and needs no further input from you. - To join an active, ongoing technical debate. @@ -171,16 +171,16 @@ Your primary goal is to gather information for maintainers efficiently. Only com ## COMMENT STYLE -Don't list all the reasons an issue is high quality or good; avoid unnecessary praise or summaries. +Don't list all reasons an issue is high quality or good; avoid unnecessary praise or summaries. Use clean, well-formatted markdown as it will be posted on GitHub. Always use explicit newline characters (`\n`) within the `comment` field of the JSON response to create line breaks for readability on GitHub. -If you are asking for multiple pieces of information in a comment, use bullet points for clarity. -Never use "we" or imply you represent the MudBlazor team or maintainers. Don't promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify the current state. +If asking for multiple pieces of information in a comment, use bullet points for clarity. +Never use "we" or imply you represent the MudBlazor team or maintainers. Don't promise or suggest that maintainers will evaluate, fix, or follow up on the issue or PR. Only state facts, ask for information, or clarify current state. Explain *why* you need information and frame advice as possibilities based on general web development practices. Don't attempt to diagnose internal workings of MudBlazor components or suggest that specific features already exist because your information might be out of date. Use "For anyone investigating this..." instead of implying a maintainer will follow up. For help questions: Answer if you can, then direct to Discussions or Discord. -For conduct violations: Be firm, explain the violation, and link to the Code of Conduct. +For conduct violations: Be firm, explain the violation, and link to Code of Conduct. Always end your comment with the disclaimer: "\n\n---\n*I'm an AI assistant — If I missed something or made a mistake, please let me know in a reply!*" **Examples:** @@ -197,11 +197,11 @@ Always end your comment with the disclaimer: "\n\n---\n*I'm an AI assistant — - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." - "This appears to be a regression that could affect many users. cc @MudBlazor/triage" -Your statements must be objective and based only on the information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: -- Instead of: "I tested the link and it's broken.", use this: "A user mentioned the reproduction link wasn't working." -- Instead of: "This helps pinpoint the performance issue.", use this: "This should help pinpoint the performance issue." -- Instead of: "It looks very similar to bug #xxxx.", use this: "It may be similar to a bug that was fixed in #xxxx." -- Instead of: "This is a useful feature.", use this: (Do not comment on the usefulness of a feature) +Your statements must be objective and based only on information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: +- Instead of: "I tested the link and it's broken.", use: "A user mentioned the reproduction link wasn't working." +- Instead of: "This helps pinpoint the performance issue.", use: "This should help pinpoint the performance issue." +- Instead of: "It looks very similar to bug #xxxx.", use: "It may be similar to a bug that was fixed in #xxxx." +- Instead of: "This is a useful feature.", use: (Do not comment on the usefulness of a feature) The examples are for guidance. Avoid using them verbatim. Try to rephrase the example to fit the specific issue context. @@ -209,7 +209,7 @@ The examples are for guidance. Avoid using them verbatim. Try to rephrase the ex You must never close an issue unless: - It has been marked as `invalid` -- It has been marked as `stale` AND meets all the rules for closing stale issues (see below) +- It has been marked as `stale` AND meets all rules for closing stale issues (see below) Don't close issues for any other reason, even if they're low quality, invalid, or missing information. Only comment or label in those cases. They will be allowed to go stale and then closed later. @@ -218,14 +218,14 @@ Don't close issues for any other reason, even if they're low quality, invalid, o Ignore all stale rules if the issue was created by a repository collaborator and remove the `stale` tag if one exists. **Mark an issue as stale if ALL of these conditions are met:** -- The issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` -- The issue does NOT have the `on hold` label -- The issue has no assignee +- Issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` +- Issue does NOT have the `on hold` label +- Issue has no assignee **Mark a pull request as stale if ALL of these conditions are met:** -- The PR has been open for at least 180 days consecutively -- The PR does NOT have the `on hold` or `breaking change` labels -- The PR has no assignee +- PR has been open for at least 180 days consecutively +- PR does NOT have the `on hold` or `breaking change` labels +- PR has no assignee ## STALE ISSUE LABEL @@ -302,18 +302,18 @@ Issue titles shouldn't include version numbers or labels that are already presen The **severity score** indicates how critical it is for a maintainer to assist this issue. This score, on a scale of 1 to 10, is a dynamic value based on the issue's impact, clarity, and age. A previously assigned score can and should be amended if new information arises, such as a surge in community interest, the discovery of a workaround, or simply due to the issue's age. -- **Age and Inactivity:** This is a primary factor. An issue's urgency decreases sharply over time to keep focus on active problems. A recent, critical bug might start at a 9, but after several months of inactivity, its score should be reduced to the low-to-moderate range (2-4). Issues older than six months should generally not be scored higher than 3, unless they are a security risk or see a major resurgence in community interest. -- **Security Vulnerabilities:** Issues representing a direct, exploitable security risk are the highest priority (**9-10**). -- **Regressions:** Bugs where a previously working feature is now broken. The score depends on the impact, ranging from minor regressions (**4-5**) to critical ones in core components that block users (**9-10**). +- **Age and Inactivity:** Primary factor. Issue urgency decreases sharply over time to keep focus on active problems. A recent, critical bug might start at a 9, but after several months of inactivity, its score should be reduced to low-to-moderate range (2-4). Issues older than six months should generally not be scored higher than 3, unless they are a security risk or see a major resurgence in community interest. +- **Security Vulnerabilities:** Issues representing direct, exploitable security risk are highest priority (**9-10**). +- **Regressions:** Bugs where a previously working feature is now broken. Score depends on impact, ranging from minor regressions (**4-5**) to critical ones in core components that block users (**9-10**). - **Widespread Impact & Blocking Issues:** Bugs affecting a broad user base or preventing essential development tasks without a simple workaround. These typically score in the high range (**7-9**). -- **Workaround Availability:** If a functional and reasonable workaround is documented in the issue, the urgency is significantly lower. This should reduce the score by 1-2 points. -- **Report Quality:** The score should reflect the effort required for triage. A clear, well-documented report with a minimal reproduction deserves a higher score (+1 point) than a vague one that lacks necessary details (-1 point). -- **Community Engagement:** High community interest (many up-votes, confirmations from different users) indicates a broader impact and can increase an issue's priority, justifying a higher score. +- **Workaround Availability:** If a functional and reasonable workaround is documented in the issue, urgency is significantly lower. This should reduce the score by 1-2 points. +- **Report Quality:** Score should reflect effort required for triage. A clear, well-documented report with minimal reproduction deserves higher score (+1 point) than a vague one that lacks necessary details (-1 point). +- **Community Engagement:** High community interest (many up-votes, confirmations from different users) indicates broader impact and can increase an issue's priority, justifying higher score. **Severity score scale:** -- **0 (None):** Assigned to non-bug issues like `enhancement`, `docs`, or `question`. Their priority is based on the project roadmap, not urgency. -- **1-3 (Low):** Minor bugs with low impact, or older issues whose urgency has decayed. *Examples: A minor cosmetic glitch, a bug with a simple workaround, an inactive bug report that is several months old.* -- **4-6 (Moderate):** General bug reports that require investigation but are not immediately blocking. *Examples: A component behaving unexpectedly but a workaround exists, a significant bug that is 1-3 months old.* -- **7-8 (High):** Important bugs impacting many users or core components without a clear workaround. These should be addressed in a reasonable timeframe. -- **9-10 (Critical):** The highest urgency. Reserved for critical security vulnerabilities, data loss bugs, or severe regressions in core functionality that block a large number of users. +- **0 (None):** Assigned to non-bug issues like `enhancement`, `docs`, or `question`. Their priority is based on project roadmap, not urgency. +- **1-3 (Low):** Minor bugs with low impact, or older issues whose urgency has decayed. *Examples: Minor cosmetic glitch, bug with simple workaround, inactive bug report that is several months old.* +- **4-6 (Moderate):** General bug reports requiring investigation but not immediately blocking. *Examples: Component behaving unexpectedly but workaround exists, significant bug that is 1-3 months old.* +- **7-8 (High):** Important bugs impacting many users or core components without clear workaround. These should be addressed in reasonable timeframe. +- **9-10 (Critical):** Highest urgency. Reserved for critical security vulnerabilities, data loss bugs, or severe regressions in core functionality that block a large number of users. From b8207289bda089a792e9644ac917cc5439a4a67f Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Tue, 29 Jul 2025 22:27:13 -0500 Subject: [PATCH 24/43] Build: Update AutoTriage (#11737) --- .github/scripts/AutoTriage.js | 83 ++++++++++++---------------- .github/workflows/issue.yml | 2 +- .github/workflows/triage-backlog.yml | 5 +- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index 388284922ea3..edd6dd721920 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -9,18 +9,17 @@ const core = require('@actions/core'); const fs = require('fs'); const path = require('path'); -// Global variables +// Global constants const AI_MODEL = 'gemini-2.5-pro'; const DB_PATH = process.env.AUTOTRIAGE_DB_PATH; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; -const GITHUB_ISSUE_NUMBER = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); +const ISSUE_NUMBER = parseInt(process.env.GITHUB_ISSUE_NUMBER, 10); const [OWNER, REPO] = (GITHUB_REPOSITORY || '').split('/'); -const issueParams = { owner: OWNER, repo: REPO, issue_number: GITHUB_ISSUE_NUMBER }; +const ISSUE_PARAMS = { owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER }; const VALID_PERMISSIONS = new Set(['label', 'comment', 'close', 'edit']); - -let PERMISSIONS = new Set( +const PERMISSIONS = new Set( (process.env.AUTOTRIAGE_PERMISSIONS || '') .split(',') .map(p => p.trim()) @@ -57,6 +56,11 @@ async function callGemini(prompt) { } ); + if (response.status === 429) { + console.error('❌ Gemini API returned 429 (Quota exceeded). Exiting and cancelling backlog.'); + process.exit(3); + } + if (response.status === 503) { console.error('❌ Gemini API returned 503 (Model overloaded). Skipping this issue.'); process.exit(2); @@ -99,14 +103,8 @@ async function buildMetadata(issue, octokit) { }; } -async function buildTimeline(octokit, issue_number) { - const { data: timelineEvents } = await octokit.rest.issues.listEventsForTimeline({ - owner: OWNER, - repo: REPO, - issue_number, - per_page: 100 - }); - +async function buildTimeline(octokit) { + const { data: timelineEvents } = await octokit.rest.issues.listEventsForTimeline({ ...ISSUE_PARAMS, per_page: 100 }); return timelineEvents.map(event => { const base = { event: event.event, actor: event.actor?.login, timestamp: event.created_at }; switch (event.event) { @@ -126,11 +124,10 @@ async function buildTimeline(octokit, issue_number) { } async function buildPrompt(issue, octokit, previousContext = null) { - let basePrompt = fs.readFileSync(path.join(__dirname, 'AutoTriage.prompt'), 'utf8'); - + const basePrompt = fs.readFileSync(path.join(__dirname, 'AutoTriage.prompt'), 'utf8'); const issueText = `${issue.title}\n\n${issue.body || ''}`; const metadata = await buildMetadata(issue, octokit); - const timelineReport = await buildTimeline(octokit, issue.number); + const timelineReport = await buildTimeline(octokit); const promptString = `${basePrompt} === SECTION: ISSUE TO ANALYZE === @@ -159,7 +156,7 @@ Your entire response must be a single, valid JSON object and nothing else. Do no } async function updateLabels(suggestedLabels, octokit) { - const { data: issue } = await octokit.rest.issues.get(issueParams); + const { data: issue } = await octokit.rest.issues.get(ISSUE_PARAMS); const currentLabels = issue.labels?.map(l => l.name || l) || []; const labelsToAdd = suggestedLabels.filter(l => !currentLabels.includes(l)); const labelsToRemove = currentLabels.filter(l => !suggestedLabels.includes(l)); @@ -175,32 +172,32 @@ async function updateLabels(suggestedLabels, octokit) { if (!octokit || !PERMISSIONS.has('label')) return; if (labelsToAdd.length > 0) { - await octokit.rest.issues.addLabels({ ...issueParams, labels: labelsToAdd }); + await octokit.rest.issues.addLabels({ ...ISSUE_PARAMS, labels: labelsToAdd }); } for (const label of labelsToRemove) { - await octokit.rest.issues.removeLabel({ ...issueParams, name: label }); + await octokit.rest.issues.removeLabel({ ...ISSUE_PARAMS, name: label }); } } async function createComment(body, octokit) { if (!octokit || !PERMISSIONS.has('comment')) return; - await octokit.rest.issues.createComment({ ...issueParams, body: body }); + await octokit.rest.issues.createComment({ ...ISSUE_PARAMS, body: body }); } async function updateTitle(title, newTitle, octokit) { console.log(`✏️ Updating title from "${title}" to "${newTitle}"`); if (!octokit || !PERMISSIONS.has('edit')) return; - await octokit.rest.issues.update({ ...issueParams, title: newTitle }); + await octokit.rest.issues.update({ ...ISSUE_PARAMS, title: newTitle }); } async function closeIssue(octokit, reason = 'not_planned') { console.log(`🔒 Closing issue as ${reason}`); if (!octokit || !PERMISSIONS.has('close')) return; - await octokit.rest.issues.update({ ...issueParams, state: 'closed', state_reason: reason }); + await octokit.rest.issues.update({ ...ISSUE_PARAMS, state: 'closed', state_reason: reason }); } -async function processIssue(issue, octokit, previousContext = null) { +async function processIssue(issue, octokit, previousContext) { const metadata = await buildMetadata(issue, octokit); const formattedMetadata = [ `#${metadata.number} (${metadata.state} ${metadata.type}) was created by ${metadata.author}`, @@ -213,9 +210,9 @@ async function processIssue(issue, octokit, previousContext = null) { const prompt = await buildPrompt(issue, octokit, previousContext); const startTime = Date.now(); const analysis = await callGemini(prompt); - const analysisTimeSeconds = ((Date.now() - startTime) / 1000).toFixed(1); + const analysisTime = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`🤖 Gemini returned analysis in ${analysisTimeSeconds}s with a severity score of ${analysis.severity}/10:`); + console.log(`🤖 Gemini returned analysis in ${analysisTime}s with a severity score of ${analysis.severity}/10:`); console.log(`🤖 "${analysis.reason}"`); await updateLabels(analysis.labels, octokit); @@ -237,37 +234,30 @@ async function processIssue(issue, octokit, previousContext = null) { return analysis; } -function getPreviousContextForIssue(triageDb, issue) { - const triageEntry = triageDb[GITHUB_ISSUE_NUMBER]; +function getPreviousTriageContext(triageDb, issue) { + const triageEntry = triageDb[ISSUE_NUMBER]; - // 1. Triage if it's never been checked. + // Triage if it never has been. if (!triageEntry) { return { lastTriaged: null, previousReasoning: 'This issue has never been triaged.' }; } - // --- Define conditions for re-triaging --- - const MS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000 const lastTriagedDate = new Date(triageEntry.lastTriaged); const timeSinceTriaged = Date.now() - lastTriagedDate.getTime(); - // 2. Triage if it's been > 14 days since the last check. - const hasExpired = timeSinceTriaged > 14 * MS_PER_DAY; - - // 3. Triage if it's been > 3 days and has a follow-up label. + // Triage if it has a follow-up label. const labels = (issue.labels || []).map(l => l.name || l); const needsFollowUp = (labels.includes('info required') || labels.includes('stale')) && - timeSinceTriaged > 3 * MS_PER_DAY; + timeSinceTriaged > 14 * 86400000; // 14 days. - // 4. Triage if the issue was updated since last triage - const issueUpdatedDate = new Date(issue.updated_at); - const wasUpdatedSinceTriaged = issueUpdatedDate > lastTriagedDate; + // Triage if the issue was updated since last triage + const wasUpdatedSinceTriaged = new Date(issue.updated_at) > lastTriagedDate; - // If any condition for re-triaging is met, return the context. - if (hasExpired || needsFollowUp || wasUpdatedSinceTriaged) { + if (wasUpdatedSinceTriaged || needsFollowUp) { return { lastTriaged: triageEntry.lastTriaged, - previousReasoning: triageEntry.previousReasoning || 'No previous reasoning available.', + previousReasoning: triageEntry.previousReasoning, }; } @@ -276,7 +266,7 @@ function getPreviousContextForIssue(triageDb, issue) { function saveArtifact(name, contents) { const artifactsDir = path.join(process.cwd(), 'artifacts'); - const filePath = path.join(artifactsDir, `${GITHUB_ISSUE_NUMBER}-${name}`); + const filePath = path.join(artifactsDir, `${ISSUE_NUMBER}-${name}`); fs.mkdirSync(artifactsDir, { recursive: true }); fs.writeFileSync(filePath, contents, 'utf8'); } @@ -295,12 +285,11 @@ async function main() { // Setup const octokit = new Octokit({ auth: GITHUB_TOKEN }); - const issue = (await octokit.rest.issues.get(issueParams)).data; - const previousContext = getPreviousContextForIssue(triageDb, issue); + const issue = (await octokit.rest.issues.get(ISSUE_PARAMS)).data; + const previousContext = getPreviousTriageContext(triageDb, issue); - // Cancel early + // We don't need to triage if (!previousContext) { - //console.log(`⏭️ #${String(GITHUB_ISSUE_NUMBER).padStart(5, '0')} does not need to be triaged right now`); process.exit(2); } @@ -311,7 +300,7 @@ async function main() { // Save database if (DB_PATH && analysis && PERMISSIONS.size > 0) { - triageDb[GITHUB_ISSUE_NUMBER] = { + triageDb[ISSUE_NUMBER] = { lastTriaged: new Date().toISOString(), previousReasoning: analysis.reason }; diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 08ce9349080c..de8d6472832a 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check out repository + - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index daf40523aa21..237275dd4fb1 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -86,7 +86,7 @@ jobs: export GITHUB_ISSUE_NUMBER="$issue_number" - # Handle script exit codes: 0=Success, 1=Fatal Error, 2=Skip Issue. + # Handle script exit codes: 0=Success, 1=Fatal Error, 2=Skip Issue, 3=Cancel All if node .github/scripts/AutoTriage.js; then count=$((count + 1)) if [ -n "$max_count" ]; then @@ -102,6 +102,9 @@ jobs: elif [ "$exit_code" -eq 2 ]; then # No-op for skippable issues. : + elif [ "$exit_code" -eq 3 ]; then + echo "🚫 Gemini API quota exceeded (exit code 3). Cancelling backlog triage." + exit 0 else echo "❌ Unexpected error (code $exit_code) on #${issue_number}" exit "$exit_code" From 8dca392eb52939498bd443b50e3c188c791d7ef8 Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:09:10 -0500 Subject: [PATCH 25/43] MudTextField: Fix Focus by ElementReference (#11733) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Daniel Chalmers --- src/MudBlazor/Base/MudBaseInput.cs | 8 -------- src/MudBlazor/Components/TextField/MudTextField.razor | 2 +- src/MudBlazor/Components/TextField/MudTextField.razor.cs | 8 ++++++++ src/MudBlazor/TScripts/mudInput.js | 8 -------- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/MudBlazor/Base/MudBaseInput.cs b/src/MudBlazor/Base/MudBaseInput.cs index 24e75784132c..20eae457ead8 100644 --- a/src/MudBlazor/Base/MudBaseInput.cs +++ b/src/MudBlazor/Base/MudBaseInput.cs @@ -788,13 +788,5 @@ private async Task UpdateInputIdStateAsync() await _inputIdState.SetValueAsync(_componentId); } - - protected async Task HandleContainerClick() - { - if (!_isFocused && IsJSRuntimeAvailable) - { - await JsRuntime.InvokeVoidAsync("mudInput.focusInput", InputElementId); - } - } } } diff --git a/src/MudBlazor/Components/TextField/MudTextField.razor b/src/MudBlazor/Components/TextField/MudTextField.razor index d4b3ea230f36..3cf5f665e9b9 100644 --- a/src/MudBlazor/Components/TextField/MudTextField.razor +++ b/src/MudBlazor/Components/TextField/MudTextField.razor @@ -18,7 +18,7 @@ Margin="@Margin" Required="@Required" ForId="@InputElementId" - @onclick="HandleContainerClick"> + @onclick="@HandleContainerClick"> @if (_mask == null) diff --git a/src/MudBlazor/Components/TextField/MudTextField.razor.cs b/src/MudBlazor/Components/TextField/MudTextField.razor.cs index e8df47d88183..d7bd253e0df2 100644 --- a/src/MudBlazor/Components/TextField/MudTextField.razor.cs +++ b/src/MudBlazor/Components/TextField/MudTextField.razor.cs @@ -232,5 +232,13 @@ private bool ShowClearButton() 0 => (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}"), _ => (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}") + $" / {Counter}" }; + + protected async Task HandleContainerClick() + { + if (!_isFocused && IsJSRuntimeAvailable && InputReference != null) + { + await InputReference.FocusAsync(); + } + } } } diff --git a/src/MudBlazor/TScripts/mudInput.js b/src/MudBlazor/TScripts/mudInput.js index be81eeeda402..41e15831f5dc 100644 --- a/src/MudBlazor/TScripts/mudInput.js +++ b/src/MudBlazor/TScripts/mudInput.js @@ -9,14 +9,6 @@ class MudInput { input.value = ''; } } - - focusInput(elementId) { - const input = document.getElementById(elementId); - if (input && document.activeElement !== input) { - input.focus(); - input.click(); - } - } } window.mudInput = new MudInput(); From fa61966eca2c77c9a01def42c3f91298bef50179 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 30 Jul 2025 14:11:39 -0500 Subject: [PATCH 26/43] MudMenu: Hide menu list if it has no items (#11740) --- src/MudBlazor/Styles/components/_menu.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/MudBlazor/Styles/components/_menu.scss b/src/MudBlazor/Styles/components/_menu.scss index 61726a47d048..29456a6a798e 100644 --- a/src/MudBlazor/Styles/components/_menu.scss +++ b/src/MudBlazor/Styles/components/_menu.scss @@ -90,7 +90,11 @@ margin-inline-end: 36px; } -// Prevent menu item hover effects peeking through rounded popovers. .mud-popover:has(> .mud-menu-list) { + // Prevent menu item hover effects peeking through rounded popovers. overflow: hidden; + // Hide the menu if it doesn't have any content. + &:has(> .mud-menu-list:empty) { + visibility: hidden; + } } From 78a9abface934e296bb740d643f35098c231a854 Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:16:50 -0500 Subject: [PATCH 27/43] MudField: AdornmentAria and ShrinkLabel (#11669) --- .../FieldLabelPlaceholderExample.razor | 66 ++++++++++++++--- .../Pages/Components/Field/FieldPage.razor | 27 +++++-- .../Field/FieldStartAdornmentTest.razor | 16 ++++ .../TestComponents/Field/FieldTest.razor | 4 +- .../Components/FieldTests.cs | 74 +++++++++++++++++++ src/MudBlazor/Components/Field/MudField.razor | 4 +- .../Components/Field/MudField.razor.cs | 29 +++++++- 7 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldStartAdornmentTest.razor create mode 100644 src/MudBlazor.UnitTests/Components/FieldTests.cs diff --git a/src/MudBlazor.Docs/Pages/Components/Field/Examples/FieldLabelPlaceholderExample.razor b/src/MudBlazor.Docs/Pages/Components/Field/Examples/FieldLabelPlaceholderExample.razor index 2f5b97e17e0d..ca9b9cdc8ad9 100644 --- a/src/MudBlazor.Docs/Pages/Components/Field/Examples/FieldLabelPlaceholderExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/Field/Examples/FieldLabelPlaceholderExample.razor @@ -1,22 +1,68 @@ -@namespace MudBlazor.Docs.Examples +@using MudBlazor.Resources +@using MudBlazor.Utilities +@inject InternalMudLocalizer Localizer +@namespace MudBlazor.Docs.Examples + + - - - -
- Switch between Label and Placeholder + + + + + ChildContent + + + Placeholder + + - + - @color + @color + + + + + + + + + ShrinkLabel + + + Placeholder + + +
@code { RenderFragment content = null; RenderFragment rf1 = @I Am Field; - string color="#6cf014"; + string color = "#6cf014"; + private bool _shrinkLabel = false; + + protected string InputClassname => + new CssBuilder("mud-input-slot") + .AddClass("mud-input-root") + .AddClass($"mud-input-root-{Variant.Outlined.ToDescriptionString()}") + .AddClass($"mud-input-root-adorned-{Adornment.End.ToDescriptionString()}") + .AddClass($"mud-input-root-margin-{Margin.Normal.ToDescriptionString()}") + .Build(); } diff --git a/src/MudBlazor.Docs/Pages/Components/Field/FieldPage.razor b/src/MudBlazor.Docs/Pages/Components/Field/FieldPage.razor index c9399c7b15d0..d76928e872bb 100644 --- a/src/MudBlazor.Docs/Pages/Components/Field/FieldPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/Field/FieldPage.razor @@ -7,8 +7,8 @@ - MudField provides the foundation for creating custom input controls with consistent styling and behavior. - It offers the same visual variants as text fields and + MudField provides the foundation for creating custom input controls with consistent styling and behavior. + It offers the same visual variants as text fields and numeric fields while allowing you to embed any custom content. Fields support labels, helper text, validation states, and adornments just like other input components. @@ -44,17 +44,30 @@ - Build custom input controls by leveraging MudField's flexible content system. When ChildContent is null, - the label automatically becomes a placeholder, enabling dynamic behavior based on content state. - This example demonstrates creating a custom color picker input using native HTML controls within a MudField wrapper. + + Build custom input controls by leveraging MudField's flexible content system. + When ChildContent is null, the label automatically becomes a placeholder, + enabling dynamic behavior based on content or adornment. + + + Use the ShrinkLabel parameter to override this behavior: + when set to true, the label remains inside the field regardless of state, + which is useful when you want to suppress automatic shrinking without causing the content to re-render + or you want to maintain a custom portion of your ChildContent such as an extra ending adornment. + + + Anytime the MudField has focus within the label will shrink and + not act as a placeholder. + - Related Components: For ready-made input controls, see TextField and - NumericField which are built on top of MudField. + Related Components: For ready-made input controls, see TextField and + NumericField which are built nearly identical to MudField, + except they have a built-in input. diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldStartAdornmentTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldStartAdornmentTest.razor new file mode 100644 index 000000000000..54120863911f --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldStartAdornmentTest.razor @@ -0,0 +1,16 @@ + + + + + Some Content Here + + + + Some Content Here + + + +@code { + public static string __description__ = "Issue 7533, testing that the Start Adornment is not going two lines with the label"; + +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldTest.razor index b42ee2eccb5c..b607e43f611a 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Field/FieldTest.razor @@ -5,7 +5,7 @@ - + @@ -17,4 +17,4 @@ @code { public static string __description__ = "Issue 2150, testing that the Adornment is not overlapping the label"; -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests/Components/FieldTests.cs b/src/MudBlazor.UnitTests/Components/FieldTests.cs new file mode 100644 index 000000000000..fbb762b18146 --- /dev/null +++ b/src/MudBlazor.UnitTests/Components/FieldTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) MudBlazor 2021 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Bunit; +using FluentAssertions; +using MudBlazor.UnitTests.TestComponents.Field; +using NUnit.Framework; + +namespace MudBlazor.UnitTests.Components +{ + [TestFixture] + public class FieldTests : BunitTest + { + [Test] + public void FieldTest_ShouldRender_Variants() + { + var comp = Context.RenderComponent(); + var fields = comp.FindAll(".mud-grid .mud-input-control.mud-field"); + fields.Should().HaveCount(3); + fields[0].ClassList.Should().Contain("mud-input-text-with-label"); + fields[0].TextContent.Trim().Should().Be("Standard"); + fields[1].ClassList.Should().Contain("mud-input-filled-with-label"); + fields[1].TextContent.Trim().Should().Be("Filled"); + fields[2].ClassList.Should().Contain("mud-input-outlined-with-label"); + fields[2].TextContent.Trim().Should().Be("OutlinedOutlined"); // Outlined includes a special fieldset label + } + + [Test] + public void FieldTest_ShouldRender_AriaAdornment() + { + var comp = Context.RenderComponent(); + var fields = comp.FindAll(".mud-grid .mud-input-control.mud-field"); + fields.Should().HaveCount(3); + fields[0].ClassList.Should().Contain("mud-input-text-with-label"); + fields[0].TextContent.Trim().Should().Be("Standard"); + var adornmentAria = comp.Find(".mud-grid .mud-input-control.mud-field svg.mud-input-adornment-icon"); + // get what adornmentAria aria-label says + adornmentAria.GetAttribute("aria-label").Trim().Should().Be("test-aria"); + } + + [Test] + public void FieldTests_ShrinkLabel() + { + // Issue 7533, when ChildContent is null, the mud-shrink class is applied + // Add a shrink label override to the field in addition to the ChildContent + var comp = Context.RenderComponent(); + // find all the mud-fields inner area + var fields = comp.FindAll(".mud-input-control.mud-field > .mud-input-control-input-container > .mud-input"); + var fieldLabels = comp.FindAll(".mud-input-control.mud-field > .mud-input-control-input-container label"); + fields.Should().HaveCount(5); + + // with end adornment no content + fields[0].ClassList.Should().NotContain("mud-shrink"); + fieldLabels[0].TextContent.Trim().Should().Contain("What am I? (0)"); + // with start adornment + fields[1].ClassList.Should().Contain("mud-shrink"); + fieldLabels[1].TextContent.Trim().Should().Be("What am I? (1)"); + // content + fields[2].ClassList.Should().Contain("mud-shrink"); + fields[2].TextContent.Trim().Should().Be("Some Content Here"); + fieldLabels[2].TextContent.Trim().Should().Be("What am I? (2)"); + + // with shrink label override + //start adornment + fields[3].ClassList.Should().NotContain("mud-shrink"); + fieldLabels[3].TextContent.Trim().Should().Be("What am I? (3)"); + // content and end adornment + fields[4].ClassList.Should().NotContain("mud-shrink"); + fields[4].TextContent.Trim().Should().Be("Some Content Here"); + fieldLabels[4].TextContent.Trim().Should().Be("What am I? (4)"); + } + } +} diff --git a/src/MudBlazor/Components/Field/MudField.razor b/src/MudBlazor/Components/Field/MudField.razor index 8c48f00b392c..488d3528e937 100644 --- a/src/MudBlazor/Components/Field/MudField.razor +++ b/src/MudBlazor/Components/Field/MudField.razor @@ -14,6 +14,7 @@ Size="@IconSize" Text="@AdornmentText" Placement="@Adornment.Start" + AriaLabel="@AdornmentAriaLabel" AdornmentClick="@OnAdornmentClick" /> }
@@ -27,6 +28,7 @@ Size="@IconSize" Text="@AdornmentText" Placement="@Adornment.End" + AriaLabel="@AdornmentAriaLabel" AdornmentClick="@OnAdornmentClick" /> } @if (Variant == Variant.Outlined) @@ -40,4 +42,4 @@ }
- \ No newline at end of file + diff --git a/src/MudBlazor/Components/Field/MudField.razor.cs b/src/MudBlazor/Components/Field/MudField.razor.cs index e4d45e0789bf..bf1c7bacd43d 100644 --- a/src/MudBlazor/Components/Field/MudField.razor.cs +++ b/src/MudBlazor/Components/Field/MudField.razor.cs @@ -19,7 +19,12 @@ public partial class MudField : MudComponentBase .AddClass($"mud-input-adorned-{Adornment.ToDescriptionString()}", Adornment != Adornment.None) .AddClass($"mud-input-margin-{Margin.ToDescriptionString()}", () => Margin != Margin.None) .AddClass("mud-input-underline", () => Underline && Variant != Variant.Outlined) - .AddClass("mud-shrink", () => !string.IsNullOrWhiteSpace(ChildContent?.ToString()) || Adornment == Adornment.Start) + // Without the mud-shrink class, the label will become a placeholder + // Apply "mud-shrink" only if ShrinkLabel is false AND + // (there is content OR the adornment is at the start) + .AddClass("mud-shrink", + !ShrinkLabel && + (ChildContent != null || Adornment == Adornment.Start)) .AddClass("mud-disabled", Disabled) .AddClass("mud-input-error", Error && !string.IsNullOrEmpty(ErrorText)) .AddClass($"mud-typography-{Typo.ToDescriptionString()}") @@ -177,6 +182,16 @@ public partial class MudField : MudComponentBase [Category(CategoryTypes.FormComponent.Appearance)] public Color AdornmentColor { get; set; } = Color.Default; + /// + /// The aria-label for the adornment. + /// + /// + /// Defaults to null. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Appearance)] + public string? AdornmentAriaLabel { get; set; } + /// /// The size of the icon. /// @@ -212,5 +227,17 @@ public partial class MudField : MudComponentBase [Parameter] [Category(CategoryTypes.Field.Appearance)] public bool Underline { get; set; } = true; + + /// + /// Controls whether the label is displayed inside the field or shrinks above it when the field does not have focus. + /// + /// + /// Defaults to false. + /// When false, the label behaves like a placeholder and shrinks only if there is content or an adornment at the start. + /// When true, the label does not shrink and remains as a placeholder regardless of content, which may result in overlap. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Appearance)] + public bool ShrinkLabel { get; set; } } } From a0adf4104772701bfa4ea7f51fd72c3e0fe567d7 Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:24:32 -0500 Subject: [PATCH 28/43] MudDataGrid: Fix HierarchyColumn Expansion with Funcs (#11292) --- .../DataGridHierarchyColumnTest.razor | 1 + ...dHierarchyInitiallyExpandedItemsTest.razor | 71 ++++++++++ ...archyInitiallyExpandedServerDataTest.razor | 113 ++++++++++++++++ .../Components/DataGridTests.cs | 127 ++++++++++++++---- .../Other/CategoryAttributeTests.cs | 6 +- .../Components/DataGrid/HierarchyColumn.razor | 8 +- .../DataGrid/HierarchyColumn.razor.cs | 25 +--- .../Components/DataGrid/MudDataGrid.razor.cs | 108 ++++++++++++--- .../Components/DataGrid/TemplateColumn.cs | 19 ++- 9 files changed, 402 insertions(+), 76 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedItemsTest.razor create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedServerDataTest.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyColumnTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyColumnTest.razor index 1e4146ce1b9b..6ea856a045e9 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyColumnTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyColumnTest.razor @@ -18,6 +18,7 @@ @code { + public static string __description__ = "A general test for all things Row Detail / Hierarchy Column"; [Parameter] public bool RightToLeft { get; set; } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedItemsTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedItemsTest.razor new file mode 100644 index 000000000000..81c3c051346d --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedItemsTest.razor @@ -0,0 +1,71 @@ +@using System.Collections.ObjectModel + + + + + + + + + + + @($"uid = {context.Item.Name}|{context.Item.Age}|{context.Item.Status}|{Guid.NewGuid()}") + + + + +Expand All +Collapse All + + +@code { + public static string __description__ = "A test for Initially Expanded when Items is an observable collection and added after the fact."; + [Parameter] + public bool RightToLeft { get; set; } + + [Parameter] + public bool EnableHeaderToggle { get; set; } + + [Parameter] + public bool ExpandSingleRow { get; set; } + + private MudDataGrid _dataGrid = null!; + + private readonly ObservableCollection _items = []; + + private readonly IReadOnlyList _itemList = + new List + { + new Model("Sam", 56, Severity.Normal), + new Model("Alicia", 54, Severity.Info), + new Model("Ira", 27, Severity.Success), + new Model("John", 32, Severity.Warning), + new Model("Anders", 24, Severity.Error) + }; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + foreach (var model in _itemList) + { + _items.Add(model); + } + + StateHasChanged(); + } + } + + private Task ExpandAll() + { + return _dataGrid.ExpandAllHierarchy(); + } + + private Task CollapseAll() + { + return _dataGrid.CollapseAllHierarchy(); + } + + public record Model(string Name, int Age, Severity Status); +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedServerDataTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedServerDataTest.razor new file mode 100644 index 000000000000..18f8ec2d2544 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridHierarchyInitiallyExpandedServerDataTest.razor @@ -0,0 +1,113 @@ +@using System.Collections.ObjectModel + + + + + + + + + + + @($"uid = {context.Item.Name}|{context.Item.Age}|{context.Item.Status}|{Guid.NewGuid()}") + + + + + + + +Expand All +Collapse All + + +@code { + public static string __description__ = "A test for Initially Expanded when Items are from ServerData."; + [Parameter] + public bool RightToLeft { get; set; } + + [Parameter] + public bool EnableHeaderToggle { get; set; } + + [Parameter] + public bool ExpandSingleRow { get; set; } + + private MudDataGrid _dataGrid = null!; + private int _rowsPerPage = 5; + + private List _itemList = + new List + { + new Model("Sam", 56, Severity.Normal), + new Model("Alicia", 54, Severity.Info), + new Model("Ira", 27, Severity.Success), + new Model("John", 32, Severity.Warning), + new Model("Anders", 24, Severity.Error), + new Model("Anu6is", 56, Severity.Normal), + new Model("Henon", 54, Severity.Info), + new Model("ScarletKuro", 27, Severity.Success), + new Model("Garderoben", 32, Severity.Warning), + new Model("Versile2", 24, Severity.Error) + }; + + private async Task> ServerReload(GridState state) + { + await Task.Delay(200); + + IEnumerable data = _itemList; + + IOrderedEnumerable? orderedData = null; + + foreach (var sort in state.SortDefinitions) + { + Func? keySelector = sort.SortBy switch + { + nameof(Model.Name) => x => x.Name, + nameof(Model.Age) => x => x.Age, + nameof(Model.Status) => x => x.Status, + _ => null + }; + + if (keySelector == null) + continue; + + if (orderedData == null) + { + orderedData = sort.Descending + ? data.OrderByDescending(keySelector) + : data.OrderBy(keySelector); + } + else + { + orderedData = sort.Descending + ? orderedData.ThenByDescending(keySelector) + : orderedData.ThenBy(keySelector); + } + } + + var finalData = (orderedData ?? data); + + var pagedData = finalData + .Skip(state.Page * state.PageSize) + .Take(state.PageSize) + .ToList(); + + return new GridData + { + TotalItems = _itemList.Count, + Items = pagedData + }; + } + + private Task ExpandAll() + { + return _dataGrid.ExpandAllHierarchy(); + } + + private Task CollapseAll() + { + return _dataGrid.CollapseAllHierarchy(); + } + + public record Model(string Name, int Age, Severity Status); +} diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs index e9be98ec3c71..9b53846da071 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs @@ -3346,7 +3346,8 @@ public async Task DataGrid_RowDetail_ExpandCollapseAllTest() await dataGrid.InvokeAsync(() => dataGrid.Instance.CollapseAllHierarchy()); dataGrid.WaitForAssertion(() => dataGrid.Instance._openHierarchies.Count.Should().Be(0)); await dataGrid.InvokeAsync(() => dataGrid.Instance.ExpandAllHierarchy()); - dataGrid.WaitForAssertion(() => dataGrid.Instance._openHierarchies.Count.Should().Be(5)); + // one is disabled and will not be expanded + dataGrid.WaitForAssertion(() => dataGrid.Instance._openHierarchies.Count.Should().Be(4)); } [Test] @@ -3400,28 +3401,6 @@ await comp.InvokeAsync(() => }); } - [Test] - public void DataGridRowDetailInitiallyExpandedMultipleTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - var item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Ira"); - - dataGrid.Instance._openHierarchies.Should().Contain(item); - - item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Anders"); - - dataGrid.Instance._openHierarchies.Should().Contain(item); - - comp.Markup.Should().Contain("uid = Ira|27|Success|"); - comp.Markup.Should().Contain("uid = Anders|24|Error|"); - - comp.Markup.Should().NotContain("uid = Sam|56|Normal|"); - comp.Markup.Should().NotContain("uid = Alicia|54|Info|"); - comp.Markup.Should().NotContain("uid = John|32|Warning|"); - } - [Test] public void DataGridChildRowContentTest() { @@ -5237,7 +5216,7 @@ public void DataGridHeaderToggleHierarchyTest() // Click again to expand all toggleButton = headerElement.QuerySelector(".mud-hierarchy-toggle-button"); toggleButton.Click(); - comp.WaitForAssertion(() => dataGrid.Instance._openHierarchies.Count.Should().Be(5)); + comp.WaitForAssertion(() => dataGrid.Instance._openHierarchies.Count.Should().Be(4)); // one disabled } [Test] @@ -5303,8 +5282,8 @@ public async Task DataGridToggleHierarchyMethodTest() // Call ToggleHierarchy again await accessor.ToggleHierarchyAsync(); - // Now all hierarchies should be expanded - dataGrid.Instance._openHierarchies.Count.Should().Be(5); + // Now all hierarchies should be expanded (except the disabled one) + dataGrid.Instance._openHierarchies.Count.Should().Be(4); } [Test] @@ -5353,6 +5332,102 @@ public void DataGrid_HierarchyExpandSingleRowTest() dataGrid.Instance._openHierarchies.First().Should().Be(item); } + [Test] + public void DataGridRowDetailInitiallyExpandedMultipleTest() + { + // just setting Items + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + var item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Ira"); + + dataGrid.Instance._openHierarchies.Should().Contain(item); + + item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Anders"); + + dataGrid.Instance._openHierarchies.Should().Contain(item); + + comp.Markup.Should().Contain("uid = Ira|27|Success|"); + comp.Markup.Should().Contain("uid = Anders|24|Error|"); + + comp.Markup.Should().NotContain("uid = Sam|56|Normal|"); + comp.Markup.Should().NotContain("uid = Alicia|54|Info|"); + comp.Markup.Should().NotContain("uid = John|32|Warning|"); + } + + [Test] + public void DataGridRowDetailInitiallyExpandedObservableMultipleTest() + { + // updating an observable collection of items after initial load + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + var item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Ira"); + + dataGrid.Instance._openHierarchies.Should().Contain(item); + + item = dataGrid.Instance.Items.FirstOrDefault(x => x.Name == "Anders"); + + dataGrid.Instance._openHierarchies.Should().Contain(item); + + comp.Markup.Should().Contain("uid = Ira|27|Success|"); + comp.Markup.Should().Contain("uid = Anders|24|Error|"); + + comp.Markup.Should().NotContain("uid = Sam|56|Normal|"); + comp.Markup.Should().NotContain("uid = Alicia|54|Info|"); + comp.Markup.Should().NotContain("uid = John|32|Warning|"); + } + + [Test] + public async Task DataGridRowDetailInitiallyExpandedServerMultipleTest() + { + // ServerReload different pages + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.WaitForAssertion(() => comp.Markup.Should().Contain("uid = Ira|27|Success|")); + comp.Markup.Should().Contain("uid = Anders|24|Error|"); + + comp.Markup.Should().NotContain("uid = Sam|56|Normal|"); + comp.Markup.Should().NotContain("uid = Alicia|54|Info|"); + comp.Markup.Should().NotContain("uid = John|32|Warning|"); + + // Collapse Ira to ensure it remains collapsed when we return to the row + // Use LINQ to find the index of the row containing "uid = Ira" + var iraIndex = comp.FindAll("tr") + .Select((row, index) => new { row, index }) + .First(r => r.row.InnerHtml.Contains("uid = Ira")).index; + + iraIndex.Should().BeGreaterThan(0, "Expected a row above the Ira detail row"); + + // Now access the row above and find the toggle button and click it + await comp.InvokeAsync(() => comp.FindAll("tr")[iraIndex - 2].QuerySelector("button").Click()); + + // Find button with aria-label = "Next Page" + var nextButton = comp.Find("button[aria-label='Next page']"); + nextButton.Should().NotBeNull(); + nextButton.Click(); + + comp.WaitForAssertion(() => comp.Markup.Should().Contain("uid = ScarletKuro|27|Success|")); + + comp.Markup.Should().NotContain("uid = Versile2|24|Error|"); + comp.Markup.Should().NotContain("uid = Anu6is|56|Normal|"); + comp.Markup.Should().NotContain("uid = Garderoben|32|Warning|"); + comp.Markup.Should().NotContain("uid = Henon|54|Info|"); + + // go back and make sure Ira isn't re-expanded + var prevButton = comp.Find("button[aria-label='Previous page']"); + prevButton.Should().NotBeNull(); + prevButton.Click(); + + comp.WaitForAssertion(() => comp.Markup.Should().Contain("uid = Anders|24|Error|")); + + comp.Markup.Should().NotContain("uid = Ira|27|Success|"); + comp.Markup.Should().NotContain("uid = Sam|56|Normal|"); + comp.Markup.Should().NotContain("uid = Alicia|54|Info|"); + comp.Markup.Should().NotContain("uid = John|32|Warning|"); + } + [Test] public async Task DataGridShouldAllowUnsortedAscDescOnly() { diff --git a/src/MudBlazor.UnitTests/Other/CategoryAttributeTests.cs b/src/MudBlazor.UnitTests/Other/CategoryAttributeTests.cs index cff723dc1538..89eed45b5bc6 100644 --- a/src/MudBlazor.UnitTests/Other/CategoryAttributeTests.cs +++ b/src/MudBlazor.UnitTests/Other/CategoryAttributeTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using System.Reflection; +using System.Reflection; using FluentAssertions; using Microsoft.AspNetCore.Components; using NUnit.Framework; @@ -42,7 +40,7 @@ public void AllComponentPropertiesHaveCategories() typeof(MudDataGridPager<>), typeof(SelectColumn<>), typeof(HierarchyColumn<>), - + typeof(TemplateColumn<>), typeof(MudTHeadRow), typeof(MudTFootRow), typeof(MudTr), diff --git a/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor b/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor index 287ae64e4af8..08a2ed3ddd35 100644 --- a/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor +++ b/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor @@ -4,8 +4,8 @@ diff --git a/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor.cs b/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor.cs index a12329cb41dc..298bbbfcdf55 100644 --- a/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor.cs +++ b/src/MudBlazor/Components/DataGrid/HierarchyColumn.razor.cs @@ -15,9 +15,6 @@ namespace MudBlazor; /// public partial class HierarchyColumn<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : MudComponentBase { - private bool _finishedInitialExpanded; - private readonly HashSet> _initiallyExpandedItems = []; - /// /// Displays the content right-to-left. /// @@ -136,32 +133,18 @@ public partial class HierarchyColumn<[DynamicallyAccessedMembers(DynamicallyAcce [Parameter] public RenderFragment> CellTemplate { get; set; } +#nullable enable /// /// The function which determines whether the row should be initially expanded. /// /// /// This function takes an item of type as input and returns a boolean indicating - /// whether the row should be expanded. + /// whether the row should be expanded. Requires item to override the Equals and GetHashCode methods. /// Defaults to a function that always returns false. /// [Parameter] - public Func InitiallyExpandedFunc { get; set; } = _ => false; - - /// - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (firstRender) - { - _finishedInitialExpanded = true; - - foreach (var context in _initiallyExpandedItems) - { - await context.Actions.ToggleHierarchyVisibilityForItemAsync.Invoke(); - } - } - } + public Func? InitiallyExpandedFunc { get; set; } +#nullable disable private string GetGroupIcon(CellContext context) { diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs index 8b4fe95d83c4..2783038b5e7d 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs @@ -32,6 +32,9 @@ public partial class MudDataGrid<[DynamicallyAccessedMembers(DynamicallyAccessed private bool _filtersMenuVisible = false; private bool _columnsPanelVisible = false; internal HashSet _openHierarchies = []; + private readonly HashSet _initialExpansions = []; + private Func _initialExpandedFunc = null; + private Func _buttonDisabledFunc = null; private string _columnsPanelSearch = string.Empty; private MudDropContainer> _dropContainer; private MudDropContainer> _columnsPanelDropContainer; @@ -736,28 +739,71 @@ public IEnumerable Items _items = value; - if (PagerStateHasChangedEvent != null) - InvokeAsync(PagerStateHasChangedEvent); + OnPagerStateChanged(); + SetupGrouping(); + ApplyInitialExpansionForItems(_items); + SetupCollectionChangeTracking(); + } + } + + + private void OnPagerStateChanged() + { + if (PagerStateHasChangedEvent is not null) + InvokeAsync(PagerStateHasChangedEvent); + } + + private void SetupGrouping() + { + if (Groupable) + GroupItems(); + } + + private void SetupCollectionChangeTracking() + { + if (_items is INotifyCollectionChanged changed) + { + changed.CollectionChanged += (s, e) => + { + _currentRenderFilteredItemsCache = null; + + if (Groupable) + GroupItems(); - // set initial grouping - if (Groupable) + ApplyInitialExpansionForNewItems(e); + }; + } + } + + private void ApplyInitialExpansionForNewItems(NotifyCollectionChangedEventArgs e) + { + if (_initialExpandedFunc is null || e.NewItems is null) + return; + + foreach (T item in e.NewItems) + { + if (_initialExpandedFunc.Invoke(item) && _initialExpansions.Add(item)) { - GroupItems(); + _openHierarchies.Add(item); } + } + } - // Setup ObservableCollection functionality. - if (_items is INotifyCollectionChanged changed) + private void ApplyInitialExpansionForItems(IEnumerable items) + { + if (_initialExpandedFunc is null || items is null) + return; + + foreach (var item in items) + { + if (_initialExpandedFunc.Invoke(item) && _initialExpansions.Add(item)) { - changed.CollectionChanged += (s, e) => - { - _currentRenderFilteredItemsCache = null; - if (Groupable) - GroupItems(); - }; + _openHierarchies.Add(item); } } } + /// /// Shows a loading animation while querying data. /// @@ -1373,7 +1419,6 @@ internal async Task InvokeServerLoadFunc() // Cancel any prior request CancelServerDataToken(); await _mudVirtualize.RefreshDataAsync(); - StateHasChanged(); } else { @@ -1396,7 +1441,6 @@ internal async Task InvokeServerLoadFunc() _currentRenderFilteredItemsCache = null; Loading = false; - StateHasChanged(); } } else @@ -1420,9 +1464,22 @@ internal async Task InvokeServerLoadFunc() CurrentPage = 0; Loading = false; - StateHasChanged(); PagerStateHasChangedEvent?.Invoke(); } + // handle initial hierarchy expansion + if (_initialExpandedFunc is not null) + { + foreach (var data in _serverData.Items) + { + // ensure we only add it once if they were expanded initially + if (_initialExpandedFunc(data) && _initialExpansions.Add(data)) + { + _openHierarchies.Add(data); + } + } + } + + StateHasChanged(); GroupItems(); } @@ -1430,6 +1487,20 @@ internal void AddColumn(Column column) { if (column.Tag?.ToString() == "hierarchy-column") { + if (column is TemplateColumn templateColumn) + { + _initialExpandedFunc = templateColumn.InitiallyExpandedFunc; + _buttonDisabledFunc = templateColumn.ButtonDisabledFunc; + // Apply expansion now if items or _serverData.Items is already set + if (_items is not null) + { + ApplyInitialExpansionForItems(_items); + } + else if (_serverData?.Items?.Any() == true) + { + ApplyInitialExpansionForItems(_serverData.Items); + } + } RenderedColumns.Insert(0, column); } else if (column.Tag?.ToString() == "select-column") @@ -2295,7 +2366,7 @@ private async Task ToggleGroupExpandRecursively(bool expanded) public async Task ExpandAllHierarchy() { _openHierarchies.Clear(); - _openHierarchies.UnionWith(FilteredItems); + _openHierarchies.UnionWith(FilteredItems.Where(x => !_buttonDisabledFunc(x))); await InvokeAsync(StateHasChanged); } @@ -2304,7 +2375,7 @@ public async Task ExpandAllHierarchy() /// public async Task CollapseAllHierarchy() { - _openHierarchies.Clear(); + _openHierarchies.RemoveWhere(x => !_buttonDisabledFunc(x)); await InvokeAsync(StateHasChanged); } @@ -2329,6 +2400,7 @@ public async Task ToggleHierarchyVisibilityAsync(T item) await InvokeAsync(StateHasChanged); } + #region Resize feature [Inject] private IEventListenerFactory EventListenerFactory { get; set; } diff --git a/src/MudBlazor/Components/DataGrid/TemplateColumn.cs b/src/MudBlazor/Components/DataGrid/TemplateColumn.cs index e9279fe022ce..43653966c4b2 100644 --- a/src/MudBlazor/Components/DataGrid/TemplateColumn.cs +++ b/src/MudBlazor/Components/DataGrid/TemplateColumn.cs @@ -2,7 +2,6 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; @@ -71,5 +70,23 @@ protected internal override void SetProperty(object item, object value) /// [Parameter] public override bool? ShowColumnOptions { get; set; } = false; + + /// + /// Sets the initial expansion state of this column if used as a Hierarchy Column. + /// + /// + /// Used internally for Hierarchy Columns, toggling will have no effect. + /// + [Parameter] + public Func? InitiallyExpandedFunc { get; set; } + + /// + /// Sets the function which determines whether buttons are disabled if used in a Hierarchy Column. + /// + /// + /// Used internally for Hierarchy Columns, setting this will have no effect. + /// + [Parameter] + public Func ButtonDisabledFunc { get; set; } = _ => false; } } From 6d3c3e875a630bfe2c90c9fad1c501830228a930 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 30 Jul 2025 18:27:43 -0500 Subject: [PATCH 29/43] Tests: Attempt to fix two flaky tests (#11744) --- .../Components/DataGridTests.cs | 139 +++++------------- .../Components/DatePickerTests.cs | 9 +- 2 files changed, 47 insertions(+), 101 deletions(-) diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs index 9b53846da071..dcb46e9e1d39 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs @@ -2784,111 +2784,52 @@ public async Task DataGridCloseFiltersTest() var dataGrid = comp.FindComponent>(); IElement FilterButton() => dataGrid.FindAll(".filter-button")[0]; - // click on the filter button - FilterButton().Click(); - - // check the number of filters displayed in the filters panel is 1 - comp.FindAll(".filters-panel .mud-grid-item.d-flex").Count.Should().Be(1); - - // Wait for the filter panel to render properly - comp.WaitForState(() => comp.FindAll(".filter-operator").Count > 0, timeout: TimeSpan.FromSeconds(5)); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - //set operator to CONTAINS - comp.FindAll(".mud-list .mud-list-item")[0].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); - - //set operator to NOT CONTAINS - FilterButton().Click(); - - // Wait for the filter panel to render properly - comp.WaitForState(() => comp.FindAll(".filter-operator").Count > 0, timeout: TimeSpan.FromSeconds(5)); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[1].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); - - //set operator to EQUALS - FilterButton().Click(); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[2].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); - - //set operator to NOT EQUALS - FilterButton().Click(); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[3].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); - - //set operator to STARTS WITH - FilterButton().Click(); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[4].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); - - //set operator to ENDS WITH - FilterButton().Click(); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[5].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should be removed since no value is provided - dataGrid.Instance.FilterDefinitions.Count.Should().Be(0); + // Helper method to select a filter operator and verify the outcome + async Task SelectFilterOperator(int operatorIndex, int expectedFilterCount) + { + // Ensure the filter panel is open before interacting + if (comp.FindAll(".filters-panel .mud-grid-item.d-flex").Count == 0) + { + FilterButton().Click(); + comp.WaitForElement(".filter-operator"); + } - //set operator to IS EMPTY - FilterButton().Click(); + // Open the operator dropdown and select an item + await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); + var listItems = comp.WaitForElements(".mud-list .mud-list-item"); + listItems[operatorIndex].Click(); - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); + // Click the overlay to close the dropdown and commit the selection + comp.Find(".mud-overlay").Click(); - comp.FindAll(".mud-list .mud-list-item")[6].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); + // Assert that the number of active filters is correct + comp.WaitForAssertion(() => + { + dataGrid.Instance.FilterDefinitions.Count.Should().Be(expectedFilterCount); + }); - //should maintain filter, no value is required - dataGrid.Instance.FilterDefinitions.Count.Should().Be(1); + // Close the filter panel to ensure a clean state for the next test + if (comp.FindAll(".filters-panel .mud-grid-item.d-flex").Count > 0) + { + FilterButton().Click(); + } + } - //set operator to IS NOT EMPTY + // 1. Initial state: Open the filter panel and confirm it's visible FilterButton().Click(); - - await comp.Find(".filter-operator").MouseDownAsync(new MouseEventArgs()); - - comp.FindAll(".mud-list .mud-list-item")[7].Click(); - comp.Find(".mud-overlay").Click(); - comp.Render(); - - //should maintain filter, no value is required - dataGrid.Instance.FilterDefinitions.Count.Should().Be(1); + comp.WaitForAssertion(() => comp.FindAll(".filters-panel .mud-grid-item.d-flex").Count.Should().Be(1)); + + // 2. Test operators that should be removed when their value is empty + await SelectFilterOperator(0, 0); // "contains" + await SelectFilterOperator(1, 0); // "not contains" + await SelectFilterOperator(2, 0); // "equals" + await SelectFilterOperator(3, 0); // "not equals" + await SelectFilterOperator(4, 0); // "starts with" + await SelectFilterOperator(5, 0); // "ends with" + + // 3. Test operators that are valid without a value + await SelectFilterOperator(6, 1); // "is empty" + await SelectFilterOperator(7, 1); // "is not empty" } [Test] diff --git a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs index 614d07b201e0..ee06289867a6 100644 --- a/src/MudBlazor.UnitTests/Components/DatePickerTests.cs +++ b/src/MudBlazor.UnitTests/Components/DatePickerTests.cs @@ -463,11 +463,16 @@ public void DatePickerStaticWithPickerActionsDayClick_Test() picker.Markup.Should().Contain("mud-selected"); //confirm selected date is shown - comp.SelectDate("23"); + // Calculate expected date before selection var date = DateTime.Today.Subtract(TimeSpan.FromDays(60)); + var expectedDate = new DateTime(date.Year, date.Month, 23); + + // Select the date + comp.SelectDate("23"); - picker.Instance.Date.Should().Be(new DateTime(date.Year, date.Month, 23)); + // Wait for the date picker to update its state after selection + comp.WaitForAssertion(() => picker.Instance.Date.Should().Be(expectedDate)); } [Test] From 2471bfeebc0558a2ffb9e05dfd1fb0c8e692ffc2 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 30 Jul 2025 18:56:28 -0500 Subject: [PATCH 30/43] Build: Update AutoTriage (#11746) --- .github/scripts/AutoTriage.js | 25 ++++++------------------- .github/scripts/AutoTriage.prompt | 23 +++++++++++++++-------- .github/workflows/triage-backlog.yml | 6 ++---- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/.github/scripts/AutoTriage.js b/.github/scripts/AutoTriage.js index edd6dd721920..0aec86cfa31a 100644 --- a/.github/scripts/AutoTriage.js +++ b/.github/scripts/AutoTriage.js @@ -234,10 +234,8 @@ async function processIssue(issue, octokit, previousContext) { return analysis; } -function getPreviousTriageContext(triageDb, issue) { - const triageEntry = triageDb[ISSUE_NUMBER]; - - // Triage if it never has been. +function getPreviousTriageContext(triageEntry) { + // Triage for the first time. if (!triageEntry) { return { lastTriaged: null, previousReasoning: 'This issue has never been triaged.' }; } @@ -245,20 +243,9 @@ function getPreviousTriageContext(triageDb, issue) { const lastTriagedDate = new Date(triageEntry.lastTriaged); const timeSinceTriaged = Date.now() - lastTriagedDate.getTime(); - // Triage if it has a follow-up label. - const labels = (issue.labels || []).map(l => l.name || l); - const needsFollowUp = - (labels.includes('info required') || labels.includes('stale')) && - timeSinceTriaged > 14 * 86400000; // 14 days. - - // Triage if the issue was updated since last triage - const wasUpdatedSinceTriaged = new Date(issue.updated_at) > lastTriagedDate; - - if (wasUpdatedSinceTriaged || needsFollowUp) { - return { - lastTriaged: triageEntry.lastTriaged, - previousReasoning: triageEntry.previousReasoning, - }; + // Recheck after 14 days for stale checks and prompt updates. + if (timeSinceTriaged > 14 * 86400000) { + return { lastTriaged: triageEntry.lastTriaged, previousReasoning: triageEntry.previousReasoning }; } return null; // Otherwise, no triage is needed. @@ -286,7 +273,7 @@ async function main() { // Setup const octokit = new Octokit({ auth: GITHUB_TOKEN }); const issue = (await octokit.rest.issues.get(ISSUE_PARAMS)).data; - const previousContext = getPreviousTriageContext(triageDb, issue); + const previousContext = getPreviousTriageContext(triageDb[ISSUE_NUMBER]); // We don't need to triage if (!previousContext) { diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index b0e08ade33ce..228d5dc56d06 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -47,7 +47,7 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "enhancement": "A new feature or improvement. Primary issue type." "good first issue": "Well-defined, uncontroversial, and simple issue suitable for new contributors" "has workaround": "Indicates reasonable, functional, albeit temporary, solution exists for the reported bug" -"help wanted": "Bug fix or feature with clear community interest that is ready for a contributor to work on." +"help wanted": "A bug fix or feature that has been open for some time, has clear community interest, and is ready for a contributor to work on." "info required": "Issue is blocked pending necessary details from the author for triage" "invalid": "Action label indicating blatant spam or violation of community standards" "localization": "Concerns support for multiple languages or regional formats" @@ -63,13 +63,15 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "security": "Impacts application security, including vulnerabilities or data protection" "stale": "Indicates an issue is inactive and will be closed if no further updates occur" "tests": "Relates to unit, integration, or other automated testing frameworks" -"urgent": "High priority issue or PR requiring immediate attention due to severity, impact, or time sensitivity" +"triage needed": "Applied to urgent issues or pull requests, or when the author is making a genuine effort to engage with maintainers but no collaborator has responded" **Labels that only human maintainers can apply (never suggest, apply, or remove these):** "answered": "Issue has been answered and does not require further action" -"not planned": "Issue or feature request is not planned for implementation" "duplicate": "Issue is a duplicate of another issue" "fixed": "Issue has been resolved or fixed in a recent update" +"not a bug": "The reported behavior is intended" +"not planned": "Issue or feature request is not planned for implementation" +"on hold": "We need to wait for something else to happen such as releasing a major version or a dependency update" ## LABELING GUIDELINES @@ -95,6 +97,11 @@ When to apply specific labels: - Does not apply to pull requests - Action label for user seeking help and not reporting a bug or requesting a feature. - Always post a comment explaining why label was added and direct to appropriate community channels. +- 'triage needed': + - Apply to urgent issues or pull requests requiring immediate attention due to severity, impact, or time sensitivity. + - Also apply to issues or pull requests where the author is making a genuine effort to work with maintainers (e.g., providing details, responding to feedback, or constructively following up), but no collaborator has responded in over a month (compare last collaborator reply date to "Current triage date"). + - Do NOT apply if the author is only posting 'bump', 'any update?', or similar non-constructive comments, because that will reset the timer. + - This label is for surfacing items that need a maintainer's attention, not for low-effort follow-ups. - 'regression': Apply with 'bug' to indicate high-priority bug where a feature that previously worked is now broken. ## COMMON ISSUE TYPES @@ -140,7 +147,7 @@ Primary goal: gather information for maintainers efficiently. Only comment when **Prioritize Commenting For:** - **Human Needed:** - - When severity score is 8 or higher, add `urgent` label and post comment including `cc @MudBlazor/triage` to notify human maintainers. + - When severity score is 8 or higher, add `triage needed` label and post comment including `cc @MudBlazor/triage` to notify human maintainers. - In the comment, explain what you need help with. - **Missing Information for Triage (`info required`):** - If `info required` label is added, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. @@ -219,12 +226,12 @@ Ignore all stale rules if the issue was created by a repository collaborator and **Mark an issue as stale if ALL of these conditions are met:** - Issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` -- Issue does NOT have the `on hold` label +- Issue does NOT have the `on hold`, `breaking change`, or `triage needed` label - Issue has no assignee **Mark a pull request as stale if ALL of these conditions are met:** -- PR has been open for at least 180 days consecutively -- PR does NOT have the `on hold` or `breaking change` labels +- PR has had no activity for at least 90 days consecutively +- PR does NOT have the `on hold`, `breaking change`, or `triage needed` labels - PR has no assignee ## STALE ISSUE LABEL @@ -260,7 +267,7 @@ If you're still experiencing this problem, please open a new issue with updated **Close a stale PR if:** - It has the `stale` label -- It has been stale for at least 90 additional days (270 consecutive days total since last activity) +- It has been stale for at least 90 additional days (180 consecutive days total since last activity) - Post this closing comment: ``` This pull request has been closed due to inactivity. diff --git a/.github/workflows/triage-backlog.yml b/.github/workflows/triage-backlog.yml index 237275dd4fb1..657aa1ae3169 100644 --- a/.github/workflows/triage-backlog.yml +++ b/.github/workflows/triage-backlog.yml @@ -72,7 +72,8 @@ jobs: max_count="" else echo "Analyzing up to $MAX_ISSUES issues" - issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc -label:enhancement -label:"on hold" -is:locked' --json number --jq '.[].number') + issue_numbers=$(gh issue list --state open --limit 9999 --search 'sort:updated-desc' --json number --jq '.[].number') + issue_numbers+=" $(gh pr list --state open --limit 9999 --search 'sort:updated-desc' --json number --jq '.[].number')" max_count="$MAX_ISSUES" fi @@ -97,16 +98,13 @@ jobs: else exit_code=$? if [ "$exit_code" -eq 1 ]; then - echo "❌ Fatal error on #${issue_number}" exit 1 elif [ "$exit_code" -eq 2 ]; then # No-op for skippable issues. : elif [ "$exit_code" -eq 3 ]; then - echo "🚫 Gemini API quota exceeded (exit code 3). Cancelling backlog triage." exit 0 else - echo "❌ Unexpected error (code $exit_code) on #${issue_number}" exit "$exit_code" fi fi From 6f65816670b3717f4310784c4619fc54d3139597 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 30 Jul 2025 21:43:38 -0500 Subject: [PATCH 31/43] Update AutoTriage.prompt --- .github/scripts/AutoTriage.prompt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 228d5dc56d06..ff0d4b808504 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -63,7 +63,7 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "security": "Impacts application security, including vulnerabilities or data protection" "stale": "Indicates an issue is inactive and will be closed if no further updates occur" "tests": "Relates to unit, integration, or other automated testing frameworks" -"triage needed": "Applied to urgent issues or pull requests, or when the author is making a genuine effort to engage with maintainers but no collaborator has responded" +"triage needed": "Applied to issues or pull requests that are urgent, or when the author is making a genuine effort in follow up comments to engage with maintainers but no collaborator has responded in over two weeks" **Labels that only human maintainers can apply (never suggest, apply, or remove these):** "answered": "Issue has been answered and does not require further action" @@ -146,9 +146,6 @@ Consider issue's age and update frequency. Note engagement like comments and rea Primary goal: gather information for maintainers efficiently. Only comment when necessary to move an issue forward. **Prioritize Commenting For:** -- **Human Needed:** - - When severity score is 8 or higher, add `triage needed` label and post comment including `cc @MudBlazor/triage` to notify human maintainers. - - In the comment, explain what you need help with. - **Missing Information for Triage (`info required`):** - If `info required` label is added, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. - If key details are missing (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. @@ -202,7 +199,6 @@ Always end your comment with the disclaimer: "\n\n---\n*I'm an AI assistant — - "This might be related to browser caching. Have you tried it in incognito mode or a private window to see if that resolves it?" - "This appears to involve static rendering, which isn't supported in MudSelect and certain other components. You might find more information in the [render modes documentation](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) or this [discussion](https://github.com/MudBlazor/MudBlazor/discussions/7430)." - "This violates our [Code of Conduct](https://github.com/MudBlazor/MudBlazor/blob/dev/CODE_OF_CONDUCT.md). Please keep discussions respectful and constructive." -- "This appears to be a regression that could affect many users. cc @MudBlazor/triage" Your statements must be objective and based only on information in the issue. Avoid making authoritative judgments or implying you can test code. Use the following guidelines for your tone: - Instead of: "I tested the link and it's broken.", use: "A user mentioned the reproduction link wasn't working." @@ -252,7 +248,7 @@ If no response is received, it will be automatically closed. ``` Hi, this pull request hasn't had activity in a while and has been marked as stale. -Please let us know if you're still working on it! @MudBlazor/triage +Please reply if you're still working on it! ``` ## CLOSING STALE ISSUES From 315a007214e5e576839f93badddfd7e0365c4e6e Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Wed, 30 Jul 2025 22:18:50 -0500 Subject: [PATCH 32/43] Update AutoTriage.prompt --- .github/scripts/AutoTriage.prompt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index ff0d4b808504..6c727a295b88 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -65,14 +65,6 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir "tests": "Relates to unit, integration, or other automated testing frameworks" "triage needed": "Applied to issues or pull requests that are urgent, or when the author is making a genuine effort in follow up comments to engage with maintainers but no collaborator has responded in over two weeks" -**Labels that only human maintainers can apply (never suggest, apply, or remove these):** -"answered": "Issue has been answered and does not require further action" -"duplicate": "Issue is a duplicate of another issue" -"fixed": "Issue has been resolved or fixed in a recent update" -"not a bug": "The reported behavior is intended" -"not planned": "Issue or feature request is not planned for implementation" -"on hold": "We need to wait for something else to happen such as releasing a major version or a dependency update" - ## LABELING GUIDELINES Only suggest labels from the VALID LABELS list. Never attempt to create new labels. From b9cffa9dc0b64309f337425fe6ad100060c3e047 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Thu, 31 Jul 2025 12:12:33 -0500 Subject: [PATCH 33/43] Update AutoTriage.prompt --- .github/scripts/AutoTriage.prompt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/scripts/AutoTriage.prompt b/.github/scripts/AutoTriage.prompt index 6c727a295b88..a51e48100cab 100644 --- a/.github/scripts/AutoTriage.prompt +++ b/.github/scripts/AutoTriage.prompt @@ -25,9 +25,9 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir - MudBlazor is a Blazor component framework with Material Design - Written in C#, Razor, and CSS with minimal JavaScript - Cross-platform support (Server, WebAssembly, MAUI) -- Reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version - Accepted reproduction sites: try.mudblazor.com, github.com, or docs on mudblazor.com - Generic placeholder "https://try.mudblazor.com/snippet" with nothing after "snippet" counts as missing reproduction. Should look like "https://try.mudblazor.com/snippet/GOcpOVQqhRGrGiGV". + - Reproduction site [https://try.mudblazor.com](https://try.mudblazor.com) is always on the latest version - Version migration guides: [https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md](https://github.com/MudBlazor/MudBlazor/blob/dev/MIGRATION.md) - Templates: [https://github.com/MudBlazor/Templates](https://github.com/MudBlazor/Templates) - Installation guide: [https://mudblazor.com/getting-started/installation](https://mudblazor.com/getting-started/installation) @@ -68,7 +68,7 @@ Your tone should be warm, supportive, and helpful, while remaining clear and dir ## LABELING GUIDELINES Only suggest labels from the VALID LABELS list. Never attempt to create new labels. -Only remove labels from the VALID LABELS list. Never remove labels added from outside the list. +Only remove labels that are in the VALID LABELS list. Never remove labels that are not in the VALID LABELS list, as these were added by maintainers. Apply at most one from: 'bug', 'enhancement', 'docs'. @@ -95,6 +95,8 @@ When to apply specific labels: - Do NOT apply if the author is only posting 'bump', 'any update?', or similar non-constructive comments, because that will reset the timer. - This label is for surfacing items that need a maintainer's attention, not for low-effort follow-ups. - 'regression': Apply with 'bug' to indicate high-priority bug where a feature that previously worked is now broken. +- 'good first issue': Only apply to issues that are completely clear, well-defined, and have reached consensus. Never apply to ambiguous issues or those still under discussion about the approach or requirements. +- 'help wanted': Only apply when there is demonstrated ongoing community interest through multiple comments, reactions, or confirmations from different users. ## COMMON ISSUE TYPES @@ -142,6 +144,8 @@ Primary goal: gather information for maintainers efficiently. Only comment when - If `info required` label is added, you must leave a comment explaining what information is missing and why it is needed for triage, unless you or a repository collaborator have already recently asked for that information. - If key details are missing (e.g., reproduction, browser, operating system, screenshots, error messages, logs, clear use case, still present in latest version), you must comment explaining what's needed. - However, if a user has provided a reproduction after being asked, do not add a new comment requesting a reproduction again. Accept the provided reproduction and proceed, even if it's not on the accepted reproduction site list. + - Do not request additional information if the issue has an assignee, as this indicates a maintainer is already working on it. + - Do not request additional information if the issue was created by a repository collaborator. - **Usage Questions (`question` label):** - If an issue is clearly a help request and not a bug/feature, you must comment to explain the `question` label and direct the user to [GitHub Discussions](https://github.com/MudBlazor/MudBlazor/discussions) or [Discord](https://discord.gg/mudblazor). - **Stale Issues/PRs (or becoming stale):** @@ -213,7 +217,7 @@ Don't close issues for any other reason, even if they're low quality, invalid, o Ignore all stale rules if the issue was created by a repository collaborator and remove the `stale` tag if one exists. **Mark an issue as stale if ALL of these conditions are met:** -- Issue has one of these labels for at least 14 days consecutively: `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` +- A bug report has had no comments for over a year OR any issue has had one or more of these labels for at least 14 days total (consecutive time across different pre-stale labels counts cumulatively): `info required`, `question`, `answered`, `not planned`, `duplicate`, `invalid`, `fixed` - Issue does NOT have the `on hold`, `breaking change`, or `triage needed` label - Issue has no assignee From f73a3a125614274ea002948d97486e5131a00eac Mon Sep 17 00:00:00 2001 From: Alex DeLaet <152199075+Alex-DeLaet@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:06:10 -0500 Subject: [PATCH 34/43] MudStepper: Add Skipped State (#11739) (#11742) Co-authored-by: Alex DeLaet --- .../Components/StepperTests.cs | 75 +++++++++++++++++++ src/MudBlazor/Components/Stepper/MudStep.cs | 49 +++++++++++- .../Components/Stepper/MudStepper.razor | 5 ++ .../Components/Stepper/MudStepper.razor.cs | 42 ++++++++++- 4 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/MudBlazor.UnitTests/Components/StepperTests.cs b/src/MudBlazor.UnitTests/Components/StepperTests.cs index 8a77a5c39a34..4f96261205ed 100644 --- a/src/MudBlazor.UnitTests/Components/StepperTests.cs +++ b/src/MudBlazor.UnitTests/Components/StepperTests.cs @@ -658,6 +658,49 @@ Task OnPreviewInteraction(StepperInteractionEventArgs args) stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(0); } + [Test] + public void ResetButton_ShouldTriggerResetStepActionForSkippedSteps() + { + var cancel = false; + var actions = new List(); + var index = -1; + Task OnPreviewInteraction(StepperInteractionEventArgs args) + { + actions.Add(args.Action); + index = args.StepIndex; + args.Cancel = cancel; + return Task.CompletedTask; + } + var stepper = Context.RenderComponent(self => + { + self.Add(x => x.OnPreviewInteraction, OnPreviewInteraction); + self.Add(x => x.ShowResetButton, true); + self.Add(x => x.NonLinear, true); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + }); + + // clicking skip sends Skip action requests to get us in a state that reset is a valid click + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(0); + stepper.Find(".mud-stepper-button-skip").Click(); + index.Should().Be(0); + actions[0].Should().Be(StepAction.Skip); + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(1); + stepper.Find(".mud-stepper-button-skip").Click(); + index.Should().Be(1); + actions[1].Should().Be(StepAction.Skip); + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(2); + + // check that clicking reset sends Reset StepAction + stepper.Find(".mud-stepper-button-reset").Click(); + actions[2].Should().Be(StepAction.Reset); + actions[3].Should().Be(StepAction.Reset); + actions[4].Should().Be(StepAction.Reset); + actions[5].Should().Be(StepAction.Activate); + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(0); + } + [Test] public void NextButton_ShouldTriggerCompleteStepAction() { @@ -690,6 +733,38 @@ Task OnPreviewInteraction(StepperInteractionEventArgs args) stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(1); } + [Test] + public void SkipButton_ShouldTriggerSkipStepAction() + { + var cancel = false; + var action = StepAction.Reset; + var index = -1; + Task OnPreviewInteraction(StepperInteractionEventArgs args) + { + action = args.Action; + index = args.StepIndex; + args.Cancel = cancel; + return Task.CompletedTask; + } + var stepper = Context.RenderComponent(self => + { + self.Add(x => x.OnPreviewInteraction, OnPreviewInteraction); + self.Add(x => x.ShowResetButton, true); + self.Add(x => x.NonLinear, true); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + self.AddChildContent(step => { step.Add(s => s.Skippable, true); }); + }); + + // clicking skip sends Skipped action requests + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(0); + stepper.Find(".mud-stepper-button-skip").Click(); + index.Should().Be(0); + action.Should().Be(StepAction.Skip); + stepper.Instance.GetState(nameof(MudStepper.ActiveIndex)).Should().Be(1); + } + + [Test] public void BackButton_ShouldTriggerActivateStepAction() { diff --git a/src/MudBlazor/Components/Stepper/MudStep.cs b/src/MudBlazor/Components/Stepper/MudStep.cs index 0234f00a0f50..ff51a6daa408 100644 --- a/src/MudBlazor/Components/Stepper/MudStep.cs +++ b/src/MudBlazor/Components/Stepper/MudStep.cs @@ -27,12 +27,17 @@ public MudStep() .WithParameter(() => HasError) .WithEventCallback(() => HasErrorChanged) .WithChangeHandler(OnParameterChanged); + SkippedState = registerScope.RegisterParameter(nameof(Skipped)) + .WithParameter(() => Skipped) + .WithEventCallback(() => SkippedChanged) + .WithChangeHandler(OnParameterChanged); } private bool _disposed; internal ParameterState CompletedState { get; private set; } internal ParameterState DisabledState { get; private set; } internal ParameterState HasErrorState { get; private set; } + internal ParameterState SkippedState { get; private set; } internal string Styles => new StyleBuilder() .AddStyle(Style) @@ -47,6 +52,7 @@ public MudStep() new CssBuilder("mud-step-label-icon") .AddClass($"mud-{(CompletedStepColor.HasValue ? CompletedStepColor.Value.ToDescriptionString() : Parent?.CompletedStepColor.ToDescriptionString())}", CompletedState && !HasErrorState && Parent?.CompletedStepColor != Color.Default && (Parent?.ActiveStep != this || (Parent?.IsCompleted == true && Parent?.NonLinear == false))) .AddClass($"mud-{(ErrorStepColor.HasValue ? ErrorStepColor.Value.ToDescriptionString() : Parent?.ErrorStepColor.ToDescriptionString())}", HasErrorState) + .AddClass($"mud-{(SkippedStepColor.HasValue ? SkippedStepColor.Value.ToDescriptionString() : Parent?.SkippedStepColor.ToDescriptionString())}", SkippedState) .AddClass($"mud-{Parent?.CurrentStepColor.ToDescriptionString()}", Parent?.ActiveStep == this && !(Parent?.IsCompleted == true && Parent?.NonLinear == false)) .Build(); @@ -118,14 +124,14 @@ public MudStep() public Color? ErrorStepColor { get; set; } /// - /// Whether the user can skip this step. + /// The color used when this step is skipped. /// /// - /// Defaults to false. + /// Defaults to null. /// [Parameter] - [Category(CategoryTypes.List.Behavior)] - public bool Skippable { get; set; } + [Category(CategoryTypes.List.Appearance)] + public Color? SkippedStepColor { get; set; } /// /// Whether this step is completed. @@ -182,6 +188,31 @@ public MudStep() [Category(CategoryTypes.List.Behavior)] public EventCallback OnClick { get; set; } + /// + /// Whether the user can skip this step. + /// + /// + /// Defaults to false. + /// + [Parameter] + [Category(CategoryTypes.List.Behavior)] + public bool Skippable { get; set; } + /// + /// Whether this step has been skipped. + /// + /// + /// Defaults to false. + /// + [Parameter] + [Category(CategoryTypes.List.Behavior)] + public bool Skipped { get; set; } + /// + /// Occurs when has changed. + /// + [Parameter] + [Category(CategoryTypes.List.Behavior)] + public EventCallback SkippedChanged { get; set; } + protected override async Task OnInitializedAsync() { base.OnInitialized(); @@ -222,6 +253,16 @@ public async Task SetDisabledAsync(bool value, bool refreshParent = true) RefreshParent(); } + /// + /// Sets the parameter, and optionally refreshes the parent . + /// + public async Task SetSkippedAsync(bool value, bool refreshParent = true) + { + await SkippedState.SetValueAsync(value); + if (refreshParent) + RefreshParent(); + } + private void RefreshParent() { (Parent as IMudStateHasChanged)?.StateHasChanged(); diff --git a/src/MudBlazor/Components/Stepper/MudStepper.razor b/src/MudBlazor/Components/Stepper/MudStepper.razor index 197a09231de6..5ce3ccad34a4 100644 --- a/src/MudBlazor/Components/Stepper/MudStepper.razor +++ b/src/MudBlazor/Components/Stepper/MudStepper.razor @@ -17,6 +17,7 @@ .AddClass("mud-clickable", NonLinear && !step.DisabledState.Value) .AddClass("mud-step-error", step.HasErrorState.Value) .AddClass("mud-step-completed", step.CompletedState.Value) + .AddClass("mud-step-skipped", step.SkippedState.Value) .Build(); + +@code { - _componentRef.MyParameter = newValue; // BLOCK PR + private CalendarComponent _calendarRef = null!; + + private void Update() + { + _calendarRef.ShowOnlyOneCalendar = true; // BL0005 warning! + } } ``` -### 4. Missing Unit Tests for Logic +**Good Example:** -**BLOCK PR**: New C# logic without corresponding bUnit tests -- Any new method with conditional logic -- Any bug fix without regression test -- Any parameter change handler without test coverage +```razor + + -## ⚠️ High Priority Issues (Flag for Review) +@code +{ + private bool _showOnlyOne; + + private void Update() + { + _showOnlyOne = true; // Declarative approach + } +} +``` -### API Breaking Changes +### Component Design Requirements -**FLAG**: Changes to public component APIs -- Adding required parameters -- Removing or renaming public properties/methods -- Changing parameter types -- Modifying default parameter values -- Changes to EventCallback signatures +#### RTL Support -**REQUIRE**: Explicit documentation of breaking changes in PR description +- All components must support Right-to-Left (RTL) layouts +- Include `[CascadingParameter] public bool RightToLeft { get; set; }` when necessary +- Apply RTL styles at the component level -### Formatting and Style Violations +#### Documentation and Testing -**FLAG**: Incorrect indentation or formatting -```csharp -// ❌ Wrong indentation (should be 4 spaces for C#) -public class MyComponent : ComponentBase -{ - private string _field; // 2 spaces - FLAG +- Add summary comments for every public property using XML documentation +- Use `CssBuilder` for classes and styles +- Add comprehensive unit tests for any component containing logic +- CSS styling alone requires no testing -// ❌ Wrong brace placement -public void Method() { // Same line - FLAG -} -``` +#### CSS Variables + +- Use CSS variables instead of hard-coding colors or other values +- Follow the established design system patterns + +## Testing Requirements + +### Unit Testing Principles + +#### What Must Be Tested + +- All non-trivial C# logic in components +- Two-way bindable properties and their behavior +- Event handling and callbacks +- Component state changes and their effects +- Parameter validation and edge cases + +#### What Doesn't Need Testing + +- Complete rendered HTML output +- Visual appearance of components +- Simple CSS styling without logic + +### Writing bUnit Tests + +#### Best Practices ```csharp -// ✅ Correct formatting -public class MyComponent : ComponentBase -{ - private string _field; // 4 spaces +// Correct approach - don't save HTML elements in variables +var comp = ctx.RenderComponent>(); +comp.Find("input").Change("Garfield"); +comp.Find("input").Blur(); +comp.FindComponent>().Instance.Value.Should().NotBeNullOrEmpty(); +``` - public void Method() - { // New line - } -} +```csharp +// Wrong approach - HTML elements become stale after interaction +var comp = ctx.RenderComponent>(); +var textField = comp.Find("input"); // Don't do this! +textField.Change("Garfield"); +textField.Blur(); // This will fail - element is stale ``` -### Performance Anti-Patterns +#### Component Interaction -**FLAG**: Performance issues ```csharp -// ❌ Synchronous operations in async context -public async Task LoadDataAsync() -{ - var result = SomeAsyncMethod().Result; // FLAG - blocking async -} +// Always use InvokeAsync for component parameter changes +var comp = ctx.RenderComponent>(); +var textField = comp.FindComponent>().Instance; -// ❌ Missing virtualization for large lists - - @foreach (var item in thousandsOfItems) // FLAG if >100 items - { - @item.Name - } - +// Wrong +textField.Value = "Garfield"; + +// Correct +await comp.InvokeAsync(() => textField.Value = "I love dogs"); ``` -## 📋 Code Review Checklist +### Test Structure -### Component Structure -- [ ] Uses `CssBuilder` for dynamic CSS classes -- [ ] All public properties have summary comments -- [ ] Private fields use `_camelCase` naming -- [ ] Public members use `PascalCase` naming -- [ ] Files end with newline +- Create test components in MudBlazor.UnitTests.Viewer +- Write corresponding tests in MudBlazor.UnitTests +- Assert initial state correctness +- Test parameter changes and their effects +- Test user interactions and event handling +- Verify proper EventCallback invocations -### Parameter Handling -- [ ] No logic in parameter setters -- [ ] All parameters use ParameterState framework -- [ ] EventCallbacks registered with `WithEventCallback()` -- [ ] Parameter updates use `SetValueAsync()` method +## Pull Request Guidelines -### Testing Requirements -- [ ] bUnit test exists for new components -- [ ] Test coverage for all conditional logic paths -- [ ] Tests use `InvokeAsync` for parameter changes -- [ ] Tests re-query DOM elements (don't store `Find()` results) -- [ ] Regression tests for bug fixes +### PR Requirements -### Accessibility -- [ ] Semantic HTML elements used (`button`, `input`, etc.) -- [ ] ARIA attributes for custom components -- [ ] Keyboard navigation support -- [ ] Focus management implemented -- [ ] Color contrast meets WCAG AA standards +#### Content Standards -### Documentation -- [ ] New components have documentation pages -- [ ] Examples ordered simple to complex -- [ ] Visual changes include screenshots/GIFs -- [ ] Breaking changes documented in PR description +- **Single Topic**: Each PR must address only one feature, bug fix, or improvement +- **Target Branch**: Always target the `dev` branch +- **Testing**: All logic changes must include corresponding unit tests +- **Documentation**: Include documentation for new features or API changes -## 🎯 Specific Patterns to Detect +#### PR Title Format -### Anti-Pattern Detection +``` +: () +``` -Look for these problematic patterns in PRs: +**Example:** -```csharp -// Parameter setter with logic -set { _field = value; DoSomething(); } // BLOCK +``` +DateRangePicker: Fix initializing DateRange with null values (#1997) +``` -// Direct parameter assignment -SomeParameter = newValue; // BLOCK +#### Description Requirements -// Component reference parameter setting -@ref="comp" ... comp.Parameter = value; // BLOCK +- Link related issues using `Fixes #` for bugs or `Closes #` for features +- Include screenshots/videos for visual changes +- Describe what was changed and why +- List any breaking changes -// Storing Find results -var button = comp.Find("button"); // FLAG -// Later... button.Click(); // May be stale +#### Technical Requirements -// Missing async/await -public async Task Method() -{ - DoSomethingAsync().Wait(); // FLAG -} +- All tests must pass (automated CI checks) +- Code must be properly formatted +- No unnecessary refactoring +- Build successfully with no warnings +- Maintain backward compatibility unless explicitly breaking -// Hard-coded styles instead of CSS variables -style="color: #1976d2;" // FLAG - use CSS variable +### Branch Management -// Missing virtualization indicators -@foreach (var item in Items) // FLAG if Items.Count > 100 -``` +- Work on descriptive feature branches: `feature/my-new-feature` or `fix/my-bug-fix` +- Keep branches up to date by merging `dev` (don't rebase) +- Use draft PRs for work in progress -### Required Patterns +### New Component Requirements -Ensure these patterns are present: +- Must support RTL layouts +- Include comprehensive unit tests +- Use CSS variables for styling +- Add documentation page with examples +- Examples over 15 lines should be collapsible +- Include XML summary comments for all public properties -```csharp -// ParameterState registration in constructor -public ComponentName() -{ - using var registerScope = CreateRegisterScope(); - // Parameter registrations here -} +## Project Structure -// Change handlers for parameters -private async Task OnParameterChangedAsync() -{ - // Logic here, not in setter -} +### Important Directories -// Proper async usage -public async Task MethodAsync() -{ - await SomeAsyncOperation(); -} +- `src/MudBlazor/`: Core component library +- `src/MudBlazor.Docs/`: Documentation and examples +- `src/MudBlazor.UnitTests/`: bUnit test suite +- `src/MudBlazor.UnitTests.Viewer/`: Visual test runner -// bUnit test structure -[Test] -public void ComponentTest() -{ - var comp = ctx.RenderComponent(); - // Test logic with proper async handling -} -``` +### Key Files -## 📚 Quick Reference +- **Components**: `src/MudBlazor/Components/` (.razor, .razor.cs) +- **Styles**: `src/MudBlazor/Styles/components/` (.scss) +- **Enums**: `src/MudBlazor/Enums/` +- **Tests**: `src/MudBlazor.UnitTests/Components/` +- **Test Components**: `src/MudBlazor.UnitTests.Viewer/TestComponents/` -### File Types and Indentation -- `.cs`, `.razor`: 4 spaces -- `.json`, `.csproj`, `.scss`: 2 spaces +### Development Workflow -### Naming Conventions -- Private fields: `_camelCase` -- Parameters/locals: `camelCase` -- Public members: `PascalCase` +1. Fork the repository and clone locally +2. Create a descriptive feature branch +3. Make changes and test locally using MudBlazor.Docs.Server +4. Write unit tests for any logic changes +5. Run the full test suite locally +6. Create PR with proper title and description +7. Address review feedback and CI failures +8. Merge when approved and all checks pass -### Required Imports Usage -- Dynamic CSS: `CssBuilder` -- Parameter handling: `ParameterState` -- Testing: `bUnit` with `InvokeAsync` +## Continuous Integration -### PR Requirements -- Title: `: (#issue)` -- Target: `dev` branch -- All CI checks passing -- Documentation for public APIs +### Automated Checks + +- **Build Verification**: Project must compile successfully +- **Test Suite**: All unit tests must pass +- **Code Coverage**: Maintain or improve coverage metrics +- **Code Quality**: Static analysis and linting checks +- **Security Scanning**: Vulnerability detection + +### Local Development + +- Run tests locally before pushing: `dotnet test` +- Use MudBlazor.Docs.Server for development and testing +- Verify changes in MudBlazor.UnitTests.Viewer when applicable +- Format code according to .NET standards + +--- + +**Remember**: These standards exist to ensure code quality, security, and maintainability. When in doubt, err on the side of caution and ask for clarification. All contributors are expected to follow these guidelines to maintain the high quality of our codebase. diff --git a/.github/codecov.yml b/.github/codecov.yml index ccfe9e78afef..db0fcfafe345 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,5 +1,6 @@ comment: require_changes: true + only_if_failed: true coverage: status: project: @@ -10,4 +11,4 @@ coverage: default: target: 100% threshold: 15% - \ No newline at end of file + diff --git a/src/.editorconfig b/src/.editorconfig index e5bfa858d422..64877a3e57be 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -46,32 +46,42 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -# Dotnet code style settings: -[*.{cs,vb}] +# C# files +[*.cs] -# IDE0055: Fix formatting -dotnet_diagnostic.IDE0055.severity = warning +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true -# Sort using and Import directives with System.* appearing first -dotnet_sort_system_directives_first = true -dotnet_separate_import_directive_groups = false -# Avoid "this." and "Me." if not necessary -dotnet_style_qualification_for_field = false:refactoring -dotnet_style_qualification_for_property = false:refactoring -dotnet_style_qualification_for_method = false:refactoring -dotnet_style_qualification_for_event = false:refactoring - -# Use language keywords instead of framework type names for type references +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion -# Suggest more modern language features when available -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion - # Non-private static fields are PascalCase dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields @@ -155,10 +165,6 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case # error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' dotnet_diagnostic.RS2008.severity = none -# IDE0073: File header -dotnet_diagnostic.IDE0073.severity = warning -file_header_template = Copyright (c) MudBlazor 2021\nMudBlazor licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. - # IDE0035: Remove unreachable code dotnet_diagnostic.IDE0035.severity = warning @@ -168,53 +174,64 @@ dotnet_diagnostic.IDE0036.severity = warning # IDE0043: Format string contains invalid placeholder dotnet_diagnostic.IDE0043.severity = warning -# IDE0044: Make field readonly -dotnet_diagnostic.IDE0044.severity = warning - # RS0016: Only enable if API files are present dotnet_public_api_analyzer.require_api_files = true -# CSharp code style settings: -[*.cs] -# Newline settings -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left - # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion -# Prefer method-like constructs to have a block body +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members csharp_style_expression_bodied_methods = false:none csharp_style_expression_bodied_constructors = false:none csharp_style_expression_bodied_operators = false:none - -# Prefer property-like constructs to have an expression-body csharp_style_expression_bodied_properties = true:none csharp_style_expression_bodied_indexers = true:none csharp_style_expression_bodied_accessors = true:none +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent -# Suggest more modern language features when available +# Pattern matching csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion +# Other features +csharp_style_prefer_index_operator = true:none +csharp_style_prefer_range_operator = true:none +csharp_style_pattern_local_over_anonymous_function = false:none + # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true @@ -239,39 +256,72 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false -# Blocks are allowed -csharp_prefer_braces = true:silent -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -# MudBlazor Team Overrides -# Settings in editorconfig are last in wins so add overrides here +############################################################################ +# # +# MudBlazor Team Overrides # +# # +# Settings in editorconfig are last in wins so add overrides here # +# # +############################################################################ [*.{cs,razor}] -# CS1591 Missing XML comment for publicly visible type or member -# We should try and finish the documentation and remove this -# For now its too noisy +file_header_template = Copyright (c) MudBlazor 2021\nMudBlazor licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. +csharp_style_prefer_primary_constructors = true:none # IDE0290 +csharp_remove_unnecessary_imports = true:warning # IDE0005 +dotnet_style_remove_unnecessary_value_assignment = true:warning # IDE0059 +dotnet_code_style_unused_parameters = all:warning # IDE0060 +dotnet_style_prefer_compound_assignment = true:suggestion # IDE0054 +dotnet_style_prefer_simplified_conditional_expression = true:suggestion # IDE0075 +dotnet_style_remove_unnecessary_cast = true:error # IDE0090 + +# Rules that must use diagnostic syntax +# CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = none -# IDE0071 Require file header +# IDE0073: Require file header dotnet_diagnostic.IDE0073.severity = none # IDE0055: Fix formatting dotnet_diagnostic.IDE0055.severity = suggestion -# IDE0044: Make field readonly -dotnet_diagnostic.IDE0044.severity = none -# IDE0034: 'default' expression can be simplified -dotnet_diagnostic.IDE0034.severity = none -# IDE0056 Indexing can be simplified -dotnet_diagnostic.IDE0056.severity = none -# IDE0057 Substring can be simplified -dotnet_diagnostic.IDE0057.severity = none -# BL0007 Component Parameters must be auto props +# BL0007: Component parameters should be auto-properties dotnet_diagnostic.BL0007.severity = suggestion -# IDE0290 Use primary constructor -dotnet_diagnostic.IDE0290.severity = none +# CS4014: Because this call is not awaited, execution of the current method continues before the call is completed +dotnet_diagnostic.CS4014.severity = error +# IDE0052: Remove unread private members +dotnet_diagnostic.IDE0052.severity = suggestion + +# Rules that could be warnings but were added too late to the project + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_diagnostic.CA1816.severity = suggestion +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion +# CA1860: Use 'Length' or 'Count' property instead of 'Any()' +dotnet_diagnostic.CA1860.severity = suggestion +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = suggestion +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = suggestion +# CA1859: Use concrete types when possible for improved performance +dotnet_diagnostic.CA1859.severity = suggestion +# CA1830: Prefer strongly-typed Appends and Inserts on StringBuilder +dotnet_diagnostic.CA1830.severity = suggestion +# CA1866: Use char overload +dotnet_diagnostic.CA1866.severity = suggestion +# CA1862: Use 'StringComparison' method overloads for string comparisons +dotnet_diagnostic.CA1862.severity = suggestion +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = suggestion +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = suggestion +# CA2012: Use ValueTasks correctly +dotnet_diagnostic.CA2012.severity = suggestion +# CA2211: Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = suggestion +# CA1806: Do not ignore method results +dotnet_diagnostic.CA1806.severity = suggestion [MudBlazor/**/*.{cs,razor}] dotnet_style_namespace_match_folder = false resharper_check_namespace_highlighting = none [*] -end_of_line = lf \ No newline at end of file +end_of_line = lf From 1b5ac83b5c3a5d61e016030c26a66f7d348a2705 Mon Sep 17 00:00:00 2001 From: Versile Johnson II <148913404+versile2@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:30:48 -0500 Subject: [PATCH 43/43] MudPopover: MudList MaxHeight Adjustments (#11734) --- .../Popover/PopoverListMaxHeightTest.razor | 38 ++++++ src/MudBlazor/TScripts/mudPopover.js | 110 ++++++++++-------- 2 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverListMaxHeightTest.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverListMaxHeightTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverListMaxHeightTest.razor new file mode 100644 index 000000000000..9439b06885f7 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverListMaxHeightTest.razor @@ -0,0 +1,38 @@ + +MudBlazor/Issues/11730 + + + + + Demo + + + + One + + + Two + + + Three + + + Four + + + Five + + + Six + + + Seven + + + + + +@code { + public static string __description__ = "Test Popover Lists Max Height Functionality"; +} diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 264b0d81869e..12d3a23cc430 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -313,15 +313,26 @@ window.mudpopoverHelper = { popoverContentNode.style['min-width'] = (boundingRect.width) + 'px'; } - // Reset max-height if it was previously set and anchor is in bounds - if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { - popoverContentNode.style.maxHeight = null; - popoverContentNode.mudHeight = null; - } - // flipping logic if (isFlipOnOpen || isFlipAlways) { + // Reset max-height if it was previously set and anchor is in bounds + // Adjust .mud-list children if they would run off screen even after flipping + const firstChild = popoverContentNode.firstElementChild; + // Check if firstChild exists, has a classList, and is a mud-list + const isList = + firstChild && + firstChild.classList && + firstChild.classList.contains("mud-list"); + // we do it here to ensure it flips properly if more space becomes available on the other side. + if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { + popoverContentNode.style.maxHeight = null; + if (isList) { + popoverContentNode.firstElementChild.style.maxHeight = null; + } + popoverContentNode.mudHeight = null; + } + const appBarElements = document.getElementsByClassName("mud-appbar mud-appbar-fixed-top"); let appBarOffset = 0; if (appBarElements.length > 0) { @@ -525,54 +536,55 @@ window.mudpopoverHelper = { this.updatePopoverZIndex(popoverContentNode, appBarElements[0]); } - const firstChild = popoverContentNode.firstElementChild; + // height adjustment logic for mud lists + if (isList) { + const popoverStyle = popoverContentNode.style; + const listStyle = firstChild.style; + + // If there is no max height set we need to check the height + // we reset previously flipped at the start of flipping logic + // a Style setting of max-height: unset; will bypass this check + const isUnset = (val) => + val == null || val === '' || val === 'none'; + const checkHeight = isUnset(popoverStyle.maxHeight) && isUnset(listStyle.maxHeight); + + if (checkHeight) { + const overflowPadding = window.mudpopoverHelper.overflowPadding; + const isCentered = Array.from(classList).some(cls => + cls.includes('mud-popover-anchor-center') + ); + + const flipAttr = popoverContentNode.getAttribute('data-mudpopover-flip'); + const isFlippedUpward = !isCentered && ( + flipAttr === 'top' || + flipAttr === 'top-and-left' || + flipAttr === 'top-and-right' + ); + + let availableHeight; + let shouldClamp = false; - // adjust the popover position/maxheight if it or firstChild does not have a max-height set (even if set to 'none') - // exceeds the bounds and doesn't have a max-height set by the user - // maxHeight adjustments stop the minute popoverNode is no longer inside the window - // Check if max-height is set on popover or firstChild - const hasMaxHeight = popoverContentNode.style.maxHeight != '' || (firstChild && firstChild.style.maxHeight != ''); - const isList = firstChild && firstChild.classList && firstChild.classList.contains("mud-list"); - - if (!hasMaxHeight && isList) { - // in case of a reflow check it should show from top properly - let shouldShowFromTop = false; - // calculate new max height if it exceeds bounds - let newMaxHeight = window.innerHeight - top - offsetY - window.mudpopoverHelper.overflowPadding; // downwards - - // Check if this is a flipped popover showing upward - // Convert classList to an array and check if any class contains the substring - const isCentered = Array.from(classList).some(className => className.includes('mud-popover-anchor-center')); - const isFlippedUpward = !isCentered && ( // center anchors don't flip - popoverContentNode.getAttribute('data-mudpopover-flip') === 'top' || - popoverContentNode.getAttribute('data-mudpopover-flip') === 'top-and-left' || - popoverContentNode.getAttribute('data-mudpopover-flip') === 'top-and-right'); - - // moving upwards - if (top + offsetY < anchorY || top + offsetY == window.mudpopoverHelper.overflowPadding) { - shouldShowFromTop = true; - // adjust newMaxHeight if flipped upwards if (isFlippedUpward) { - newMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding - popoverNode.offsetHeight; + availableHeight = anchorY - overflowPadding - popoverNode.offsetHeight; + shouldClamp = availableHeight < popoverContentNode.offsetHeight; + if (shouldClamp) { + top = overflowPadding; + offsetY = 0; + } + } else { + // Space from popover top down to bottom of screen + const popoverTopEdge = top + offsetY; + availableHeight = window.innerHeight - popoverTopEdge - overflowPadding; + shouldClamp = popoverContentNode.offsetHeight > availableHeight; } - // adjust newMaxHeight if not flipped upwards - else { - newMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding; - } - } - // if calculated height exceeds the new maxheight - if (popoverContentNode.offsetHeight > newMaxHeight) { - if (shouldShowFromTop) { // adjust top to show from top - // also adjust newMaxHeight - top = window.mudpopoverHelper.overflowPadding; - offsetY = 0; + if (shouldClamp) { + const minVisibleHeight = overflowPadding * 3; + const newMaxHeight = Math.max(availableHeight, minVisibleHeight); + popoverContentNode.style.maxHeight = `${newMaxHeight}px`; + firstChild.style.maxHeight = `${newMaxHeight}px`; + popoverContentNode.mudHeight = "setmaxheight"; } - // set newMaxHeight to be minimum of 3x overflow padding, by default 72px (or 3 items roughly) - newMaxHeight = Math.max(newMaxHeight, window.mudpopoverHelper.overflowPadding * 3); - popoverContentNode.style.maxHeight = (newMaxHeight) + 'px'; - firstChild.style.maxHeight = (newMaxHeight) + 'px'; - popoverContentNode.mudHeight = "setmaxheight"; } } }