diff --git a/.github/prompts/bug-triage.prompt.yml b/.github/prompts/bug-triage.prompt.yml new file mode 100644 index 0000000..94cab7d --- /dev/null +++ b/.github/prompts/bug-triage.prompt.yml @@ -0,0 +1,53 @@ +messages: + - role: system + content: >+ + You are an expert software triage engineer analyzing bug reports for OKD (The Community Distribution of Kubernetes). + OKD is the community distribution of Kubernetes that powers Red Hat's OpenShift, optimized for continuous application + development and multi-tenant deployment. + + Your task is to assess whether a bug report is complete and actionable. Analyze the bug report for the following key elements: + + 1. Problem Description: Is there a clear description of what went wrong and what was expected? + 2. Reproduction Steps: Are there specific steps provided to reproduce the issue? + 3. Environment Information: Is cluster version, platform (AWS/bare metal/etc), and relevant context provided? + 4. Log Output: Are there logs, error messages, or diagnostic output (if applicable)? + + Rate the overall bug report as: + - Ready for Review - All critical information is present, the bug is clearly described, and can be worked on immediately + - Missing Details - Important information is missing or unclear (specify what's needed) + - Needs Clarification - The report is confusing or contradictory and requires clarification from the reporter + The severity and component output MUST be a single word enclosed in the header "### AI Assessment:". Concatenate the severity and component with a hyphen. + + Rate the issue's severity based on its description of impact: + - critical: System is down, major data loss, or core functionality completely broken. + - high: Significant disruption, major feature broken, or common user workflow blocked. + - medium: Minor inconvenience, visual bug, or easily worked around issue. + - low: Cosmetic issue, documentation error, or non-critical feature improvement + Also, determine the primary component affected from this list: + - CoreAPI: Kubernetes/OpenShift API server and controllers + - Networking: SDN, ingress/routes, CNI configuration + - Installation: Bare metal, AWS, or Azure install process and configuration + - Storage: Persistent Volumes, storage classes, or volume mounting + - WebConsole: UI and user experience issues + - Documentation: Errors in guides or reference material + + Example Output: `### AI Assessment: high-Networking` + Example Output: `### AI Assessment: critical-CoreAPI` + + Response Format: + Start your response with: `AI Assessment: [Severity]-[Component]` + (For example: `AI Assessment: high-Networking` or `AI Assessment: critical-CoreAPI`) + + Then provide: + 1. A brief analysis of each key element (1-2 sentences each) + 2. What specific information is missing (if any) + 3. Overall recommendation for next steps + + Keep your response under 200 words and be specific about what's missing or unclear. + - role: user + content: '{{input}}' +model: openai/gpt-4o +modelParameters: + max_tokens: 300 +testData: [] +evaluators: [] diff --git a/.github/scripts/add-label.js b/.github/scripts/add-label.js new file mode 100644 index 0000000..6379b12 --- /dev/null +++ b/.github/scripts/add-label.js @@ -0,0 +1,26 @@ +/** + * Adds the kind/bug label to an issue + */ + +async function addLabel(github, context, core, issueNumber) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['kind/bug'], + }); + core.info(`Added kind/bug label to issue #${issueNumber} after AI assessment`); + } catch (error) { + // Label might already exist, which is fine + if (error.status === 422) { + core.info(`Label kind/bug already exists on issue #${issueNumber}`); + } else { + core.warning(`Failed to add label to issue #${issueNumber}: ${error.message}`); + throw error; + } + } +} + +module.exports = { addLabel }; + diff --git a/.github/scripts/format-message.js b/.github/scripts/format-message.js new file mode 100644 index 0000000..92004a5 --- /dev/null +++ b/.github/scripts/format-message.js @@ -0,0 +1,38 @@ +/** + * Formats a simple message for both GitHub comments and Slack + * Uses plain text format that works for both platforms + */ + +function parseAssessments(assessmentOutput, core) { + let assessments = []; + try { + assessments = JSON.parse(assessmentOutput || '[]'); + } catch (e) { + if (core) { + core.warning(`Failed to parse assessment output: ${e.message}`); + } + } + return assessments; +} + +function formatMessage(issue, assessments) { + let message = `OKD Issue #${issue.number}: ${issue.title}\n`; + message += `${issue.html_url}\n\n`; + + if (!assessments || assessments.length === 0) { + message += 'No triage assessment available'; + } else { + for (const assessment of assessments) { + const label = assessment.assessmentLabel || 'N/A'; + const response = assessment.response || 'No response'; + + message += `Label: ${label}\n`; + message += `Assessment: ${response}\n`; + } + } + + return message.trim(); +} + +module.exports = { formatMessage, parseAssessments }; + diff --git a/.github/scripts/format-slack-message.js b/.github/scripts/format-slack-message.js new file mode 100644 index 0000000..f39a2dc --- /dev/null +++ b/.github/scripts/format-slack-message.js @@ -0,0 +1,62 @@ +/** + * Formats messages specifically for Slack + * Converts GitHub markdown formatting to Slack's format + */ + +/** + * Converts markdown text to Slack-compatible formatting + */ +function convertMarkdownToSlack(text) { + if (!text) return ''; + + let converted = text; + + // Convert markdown headers (### Header) to bold text + converted = converted.replace(/^###\s+(.+)$/gm, '*$1*'); + converted = converted.replace(/^##\s+(.+)$/gm, '*$1*'); + converted = converted.replace(/^#\s+(.+)$/gm, '*$1*'); + + // Convert markdown bold (**text** or __text__) to Slack bold (*text*) + converted = converted.replace(/\*\*(.+?)\*\*/g, '*$1*'); + converted = converted.replace(/__(.+?)__/g, '*$1*'); + + // Convert markdown links [text](url) to Slack links + converted = converted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<$2|$1>'); + + // Convert markdown code blocks ``` to plain text (Slack handles these differently) + converted = converted.replace(/```[\s\S]*?```/g, (match) => { + return match.replace(/```/g, ''); + }); + + // Convert inline code `text` to Slack inline code (same format) + // No change needed, Slack uses the same format + + return converted; +} + +/** + * Formats a message specifically for Slack + */ +function formatSlackMessage(issue, assessments) { + let message = `*OKD Issue #${issue.number}*: ${issue.title}\n`; + message += `<${issue.html_url}|View Issue>\n\n`; + + if (!assessments || assessments.length === 0) { + message += '_No triage assessment available_'; + } else { + for (const assessment of assessments) { + const label = assessment.assessmentLabel || 'N/A'; + const response = assessment.response || 'No response'; + + // Convert markdown in the response to Slack format + const slackFormattedResponse = convertMarkdownToSlack(response); + + message += `*Label:* ${label}\n`; + message += `*Assessment:*\n${slackFormattedResponse}\n`; + } + } + + return message.trim(); +} + +module.exports = { formatSlackMessage, convertMarkdownToSlack }; diff --git a/.github/scripts/get-issues.js b/.github/scripts/get-issues.js new file mode 100644 index 0000000..c72bf59 --- /dev/null +++ b/.github/scripts/get-issues.js @@ -0,0 +1,23 @@ +/** + * Issue-related utilities: getting issue details + */ + +/** + * Gets issue details and sets outputs + */ +async function getIssueDetails(github, context, core, issueNumber, owner, repo) { + const { data: issue } = await github.rest.issues.get({ + owner: owner, + repo: repo, + issue_number: issueNumber, + }); + + core.setOutput('issue_number', issue.number); + core.setOutput('issue_title', issue.title); + core.setOutput('issue_body', issue.body || ''); + core.setOutput('issue_url', issue.html_url); + core.setOutput('repo_name', repo); +} + +module.exports = { getIssueDetails }; + diff --git a/.github/scripts/process-assessments.js b/.github/scripts/process-assessments.js new file mode 100644 index 0000000..bce4c11 --- /dev/null +++ b/.github/scripts/process-assessments.js @@ -0,0 +1,22 @@ +/** + * Processes assessment output and formats messages for GitHub summary + */ + +const { formatMessage, parseAssessments } = require('./format-message'); + +async function processAssessments(assessmentOutput, issueNumber, issueTitle, issueUrl, core) { + const assessments = parseAssessments(assessmentOutput, core); + + const issue = { + number: parseInt(issueNumber), + title: issueTitle, + html_url: issueUrl + }; + + const message = formatMessage(issue, assessments); + core.summary.addRaw(message); + await core.summary.write(); +} + +module.exports = { processAssessments }; + diff --git a/.github/scripts/send-slack-message.js b/.github/scripts/send-slack-message.js new file mode 100644 index 0000000..8112103 --- /dev/null +++ b/.github/scripts/send-slack-message.js @@ -0,0 +1,49 @@ +/** + * Sends a Slack-formatted message to Slack + */ + +const { parseAssessments } = require('./format-message'); +const { formatSlackMessage } = require('./format-slack-message'); + +async function sendSlackMessage(issueNumber, issueTitle, issueUrl, assessmentsJson, webhookUrl, core) { + if (!webhookUrl) { + core.warning('No Slack webhook URL provided, skipping notification'); + return; + } + + const assessments = typeof assessmentsJson === 'string' + ? parseAssessments(assessmentsJson, core) + : assessmentsJson; + + const issue = { + number: parseInt(issueNumber), + title: issueTitle, + html_url: issueUrl + }; + + const message = formatSlackMessage(issue, assessments); + + const payload = { + text: message + }; + + try { + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`Slack API returned ${response.status}: ${response.statusText}`); + } + + core.info('Slack notification sent successfully'); + } catch (err) { + core.warning(`Slack notification failed: ${err.message}`); + throw err; + } +} + +module.exports = { sendSlackMessage }; + diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml new file mode 100644 index 0000000..448a8de --- /dev/null +++ b/.github/workflows/issue-opened.yml @@ -0,0 +1,97 @@ +name: "Triage New Issue" +run-name: "AI Triage for Issue #${{ github.event.issue.number }}" + +on: + issues: + types: [opened, edited] + +concurrency: + group: ai-triage-issue-${{ github.event.issue.number }} + cancel-in-progress: false + +permissions: + issues: write + contents: read + actions: read + models: read + +jobs: + triage-issues: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get Issue Details + id: get-issue + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { getIssueDetails } = require('./.github/scripts/get-issues.js'); + await getIssueDetails(github, context, core, ${{ github.event.issue.number }}, context.repo.owner, context.repo.repo); + + - name: AI Issue Assessment + id: ai-assessment + uses: github/ai-assessment-comment-labeler@v1.0.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue_number: ${{ steps.get-issue.outputs.issue_number }} + issue_body: ${{ steps.get-issue.outputs.issue_body }} + repo_name: ${{ steps.get-issue.outputs.repo_name }} + owner: ${{ github.repository_owner }} + ai_review_label: 'kind/bug' + prompts_directory: './.github/prompts' + labels_to_prompts_mapping: 'kind/bug,bug-triage.prompt.yml' + max_tokens: 300 + + - name: Add kind/bug label + if: always() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { addLabel } = require('./.github/scripts/add-label.js'); + await addLabel(github, context, core, ${{ steps.get-issue.outputs.issue_number }}); + + - name: Process Assessments + id: process-assessments + if: always() + uses: actions/github-script@v7 + env: + ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} + ISSUE_NUMBER: ${{ steps.get-issue.outputs.issue_number }} + ISSUE_TITLE: ${{ steps.get-issue.outputs.issue_title }} + ISSUE_URL: ${{ steps.get-issue.outputs.issue_url }} + with: + script: | + const { processAssessments } = require('./.github/scripts/process-assessments.js'); + await processAssessments( + process.env.ASSESSMENT_OUTPUT, + process.env.ISSUE_NUMBER, + process.env.ISSUE_TITLE, + process.env.ISSUE_URL, + core + ); + + - name: Send to Slack + if: always() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} + ISSUE_NUMBER: ${{ steps.get-issue.outputs.issue_number }} + ISSUE_TITLE: ${{ steps.get-issue.outputs.issue_title }} + ISSUE_URL: ${{ steps.get-issue.outputs.issue_url }} + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const { sendSlackMessage } = require('./.github/scripts/send-slack-message.js'); + await sendSlackMessage( + process.env.ISSUE_NUMBER, + process.env.ISSUE_TITLE, + process.env.ISSUE_URL, + process.env.ASSESSMENT_OUTPUT, + process.env.SLACK_WEBHOOK_URL, + core + );