From d37f2bfd9fe963d1d71c9db18daa613ccb8b76c2 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:41:33 +0530 Subject: [PATCH] feat(gemini): enhance issue analysis with domain context, auto-labeling & edited trigger - Fix workflow: checkout@v4, correct bot-actor guard, add npm caching - Add edited trigger on gemini_issues_review.yml (fires only on body/title changes) - Add PROJECT_CONTEXT constant with full CAP SDM architecture details - Add classifyIssue() helper: auto-labels issues by type + component name - Guard against empty/short issue bodies with a structured info-request comment - Enrich analysis prompt with PROJECT_CONTEXT and classification result - Add priorAnalysis param to handleNewIssue for refined re-analysis passes - confirm remediation: now generates a task checklist from prior analysis - refine analysis: now fetches last bot comment as prior context before re-running - hey gemini on issues: now runs repo scan + injects PROJECT_CONTEXT for precise answers - Add edited event handler in run(): nudges for refine or runs fresh analysis --- .github/scripts/review.js | 249 ++++++++++++++++----- .github/workflows/gemini_issues_review.yml | 21 +- 2 files changed, 213 insertions(+), 57 deletions(-) diff --git a/.github/scripts/review.js b/.github/scripts/review.js index 5e167785a..21ae8e8d9 100644 --- a/.github/scripts/review.js +++ b/.github/scripts/review.js @@ -89,6 +89,54 @@ async function splitDiffIntoTokens(genAI, diff, maxTokens = MAX_CHUNK_TOKENS) { return chunks; } +// Project domain context — injected into issue analysis prompts +//---------------------------------------------------------------------------------------------------------------- + +const PROJECT_CONTEXT = ` +**Project:** SAP CAP Java SDK plugin for Document Management Service (SDM / CMIS) integration. +**Purpose:** Intercepts CAP Attachments CRUD events and routes document storage to SAP Document Management Service via CMIS REST API. Distributed as a Maven JAR (Java 17) consumed by CAP Java apps via Java SPI. + +**Key Architectural Components:** +- Event Handlers (CAP @Before/@On/@After lifecycle): SDMCreateAttachmentsHandler, SDMReadAttachmentsHandler, SDMUpdateAttachmentsHandler, SDMAttachmentsServiceHandler (core SDM bridge), SDMCustomServiceHandler (COPY/MOVE operations). +- Token Management: TokenHandler singleton — OAuth2 named-user and technical-user flows; cached via EhCache 3 (userTokenCache, clientCredentialsTokenCache, 660-min TTL). +- Caching: CacheConfig (static EhCache 3) — 8 named caches: userTokenCache, clientCredentialsTokenCache, userAuthoritiesTokenCache, repoCache, secondaryTypesCache, secondaryPropertiesCache, maxAllowedAttachmentsCache, errorMessageCache. +- Upload Scan States: uploading → Success | Failed | VirusDetected | VirusScanInprogress. +- HTTP Layer: Apache HttpClient 5 for CMIS REST calls; multipart upload for document creation; RetryUtils for transient failure retry. +- Multi-tenancy: Single-tenant and multi-tenant Cloud Foundry deployments with separate integration test workflows. +- Key Dependencies: cds-services-api 3.10.3, SAP Cloud SDK 5.21.0 (OAuth2DestinationBuilder), token-client 3.5.7, EhCache 3.10.8, Apache HttpClient 5.4.4. +- Build: Maven multi-module, Spotless (Google Java Format), JaCoCo, SonarQube, Black Duck, CodeQL on GitHub Actions. +`.trim(); + +/** + * Calls Gemini to classify an issue by type and affected component. + * Returns { type, component } — never throws; falls back to defaults on error. + */ +async function classifyIssue(issueTitle, issueBody, genAI) { + const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); + const prompt = `Classify this GitHub issue for the SAP CAP Java SDM plugin project. +Issue Title: ${issueTitle} +Issue Body: ${truncate(issueBody || '', 800)} + +Respond ONLY with a valid JSON object — no markdown, no code fences, no extra text. +Required format: +{"type":"","component":""}`; + try { + const result = await fetchWithBackoff(() => model.generateContent(prompt)); + const text = result.response.text().trim().replace(/```(?:json)?\n?|\n?```/g, '').trim(); + const parsed = JSON.parse(text); + // Validate values to avoid injecting arbitrary label names + const validTypes = new Set(['bug', 'enhancement', 'question', 'security', 'documentation', 'performance']); + const validComponents = new Set(['caching', 'auth-token', 'handler', 'service', 'persistence', 'ci-cd', 'sdm-api', 'integration-test', 'build', 'docs', 'unknown']); + return { + type: validTypes.has(parsed.type) ? parsed.type : 'bug', + component: validComponents.has(parsed.component) ? parsed.component : 'unknown', + }; + } catch (e) { + console.warn("Issue classification failed:", e.message); + return { type: 'bug', component: 'unknown' }; + } +} + // Core logic functions //---------------------------------------------------------------------------------------------------------------- @@ -318,19 +366,27 @@ async function handleCommentResponse(octokit, commentBody, number, genAI) { ${userQuestion} `; } else { - // This is a comment on a regular issue. We don't have a diff. + // This is a comment on a regular issue. Include repo scan + project context for better answers. const issueTitle = context.payload.issue.title; const issueBody = context.payload.issue.body; - prompt = `A user has a question about a GitHub issue. The issue's title and body are provided below, followed by the user's question. Please provide a clear and concise answer. - - --- - Issue Title: ${issueTitle} - Issue Body: ${issueBody} - - --- - User's question: - ${userQuestion} - `; + const repoScan = scanRepositoryForIssue(issueTitle, userQuestion, process.cwd()); + const codeContext = repoScan.matches.length > 0 + ? `\n**Relevant Code Context (from repository scan):**\n${truncate(repoScan.matches.map(m => `File: ${m.file}\n${m.snippet}`).join('\n---\n'), 4000)}` + : ''; + prompt = `You are an expert engineer on the SAP CAP Java SDM plugin. A user has a question about a GitHub issue. Use the project context, issue details, and any relevant code snippets to give a precise, actionable answer. + +**Project Context:** +${PROJECT_CONTEXT} + +--- +**Issue Title:** ${issueTitle} +**Issue Body:** ${issueBody} +${codeContext} + +--- +**User's question:** +${userQuestion} +`; } let response = "Error: Could not generate a response to your comment."; @@ -355,8 +411,19 @@ async function handleCommentResponse(octokit, commentBody, number, genAI) { } } -async function handleNewIssue(octokit, owner, repo, issueNumber, issueTitle, issueBody, genAI) { +async function handleNewIssue(octokit, owner, repo, issueNumber, issueTitle, issueBody, genAI, priorAnalysis = null) { console.log(`Processing new issue #${issueNumber}: ${issueTitle}`); + + // Guard: if the issue body is missing or too short, request more details instead of attempting analysis + if (!issueBody || issueBody.trim().length < 30) { + console.log("Issue body too short or empty. Requesting more details from author."); + await octokit.rest.issues.createComment({ + owner, repo, issue_number: issueNumber, + body: `👋 Thanks for opening this issue!\n\nTo provide accurate root-cause analysis and remediation steps, could you share a bit more context?\n\n**Helpful details to include:**\n- Steps to reproduce the problem\n- Expected behaviour vs. actual behaviour\n- Relevant logs, stack traces, or error messages\n- Your environment: CAP Java SDK version, tenant type (single/multi), Cloud Foundry plan\n\n_I'll automatically re-analyse once the description is updated._`, + }); + return; + } + // Primary lightweight model for generation const flashModel = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); @@ -380,51 +447,76 @@ async function handleNewIssue(octokit, owner, repo, issueNumber, issueTitle, iss return; } - // No match: scan repository for potential causes - const repoScan = scanRepositoryForIssue(issueTitle, issueBody, process.cwd()); + // No match: classify issue and scan repository for potential causes — run in parallel + const [classification, repoScan] = await Promise.all([ + classifyIssue(issueTitle, issueBody, genAI), + Promise.resolve(scanRepositoryForIssue(issueTitle, issueBody, process.cwd())), + ]); + + console.log(`Issue classified — type: "${classification.type}", component: "${classification.component}"`); console.log(`Repository scan complete. Matched contexts: ${repoScan.matches.length}`); + + // Apply type and component labels + const typeColorMap = { + bug: 'd73a4a', enhancement: 'a2eeef', question: 'd876e3', + security: 'e4e669', documentation: '0075ca', performance: 'f9d0c4', + }; + const componentLabel = `component:${classification.component}`; + await Promise.all([ + ensureLabel(octokit, owner, repo, classification.type, { color: typeColorMap[classification.type] || 'cccccc', description: `Issue type: ${classification.type}` }), + ensureLabel(octokit, owner, repo, componentLabel, { color: '1d76db', description: `Affected component: ${classification.component}` }), + ]); + await octokit.rest.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: [classification.type, componentLabel] }); + const joinedContexts = repoScan.matches.map(m => `File: ${m.file}\n${m.snippet}`).join("\n---\n"); // --- DETAILED PROMPT FIX (from previous request) --- - const recPrompt = `You are an expert senior engineer. A new issue was filed. Use the code contexts to hypothesize root causes and generate a detailed, prioritized remediation checklist. Your output must strictly follow the required markdown structure below. + const recPrompt = `You are an expert senior engineer and specialist in the SAP CAP Java SDK ecosystem. Use the project context below to ground your analysis in the actual architecture. - Crucially, for the most likely and actionable remediation steps, you **must include the exact code snippet** showing the required change in a markdown code block. Do not just describe the fix—show the code. +**Project Context:** +${PROJECT_CONTEXT} - Format your output strictly as: - - ###### - ## 🧪 Initial Analysis & Proposed Remediation - - **Summary & Root Cause Hypothesis** - [A detailed summary of the issue, including an hypothesis on the root cause.] +**Issue Classification:** Type = ${classification.type}, Component = ${classification.component} +${priorAnalysis ? `\n⚠️ **This is a Refined Analysis.** The previous automated analysis is shown below — deepen or correct it based on updated context:\n\`\`\`\n${truncate(priorAnalysis, 2000)}\n\`\`\`\n` : ''} +A new issue was filed. Use the code contexts to hypothesize root causes and generate a detailed, prioritized remediation checklist. Your output must strictly follow the required markdown structure below. - --- - - ### 🥇 Prioritized Remediation Steps (with Code Fixes) - - 1. **Verify Annotation Placement (High Priority):** - * **Rationale:** [Explain why this is the most likely fix, e.g., technical fields require a specific placement.] - * **Action & Required Change:** [State the action clearly, followed by the specific code snippet showing the fix in CDS or a relevant configuration file (e.g., manifest.json). If no change is required, state the expected state.] - - 2. **Inspect OData $metadata Output (High Priority):** - * **Rationale:** [Explain what inspecting the metadata will confirm (backend generation vs. UI rendering issue).] - * **Action & Command:** [Provide the exact command/URL to check, e.g., \`https:///$metadata\`] - - 3. **Test UI-Level Override (Medium Priority):** - * **Rationale:** [Explain why a UI override might be necessary if the backend annotation is ignored.] - * **Action & Required Change:** [Provide the action and the specific code snippet for the change, likely in \`manifest.json\` or a similar UI config.] - - --- - - **Risk Assessment** - [A brief assessment of the risk/impact of applying the proposed fixes.] - ###### +Crucially, for the most likely and actionable remediation steps, you **must include the exact code snippet** showing the required change in a markdown code block. Do not just describe the fix—show the code. - Issue Title: ${issueTitle} - Issue Body: ${issueBody} - Relevant Code Contexts (truncated): - ${truncate(joinedContexts, 12000)} - `; +Format your output strictly as: + +###### +## 🧪 Initial Analysis & Proposed Remediation + +**Summary & Root Cause Hypothesis** +[A detailed summary of the issue, including a hypothesis on the root cause. Reference specific classes (e.g., TokenHandler, SDMServiceImpl, CacheConfig) where relevant.] + +--- + +### 🥇 Prioritized Remediation Steps (with Code Fixes) + +1. **Verify Annotation Placement (High Priority):** + * **Rationale:** [Explain why this is the most likely fix, e.g., technical fields require a specific placement.] + * **Action & Required Change:** [State the action clearly, followed by the specific code snippet showing the fix in CDS or a relevant configuration file (e.g., manifest.json). If no change is required, state the expected state.] + +2. **Inspect OData $metadata Output (High Priority):** + * **Rationale:** [Explain what inspecting the metadata will confirm (backend generation vs. UI rendering issue).] + * **Action & Command:** [Provide the exact command/URL to check, e.g., \`https:///$metadata\`] + +3. **Test UI-Level Override (Medium Priority):** + * **Rationale:** [Explain why a UI override might be necessary if the backend annotation is ignored.] + * **Action & Required Change:** [Provide the action and the specific code snippet for the change, likely in \`manifest.json\` or a similar UI config.] + +--- + +**Risk Assessment** +[A brief assessment of the risk/impact of applying the proposed fixes.] +###### + +Issue Title: ${issueTitle} +Issue Body: ${issueBody} +Relevant Code Contexts (truncated): +${truncate(joinedContexts, 12000)} +`; // --- END DETAILED PROMPT FIX --- let recommendations = "Failed to generate recommendations."; @@ -644,13 +736,38 @@ async function run() { // Maintainer confirmation flow const issueLabels = context.payload.issue.labels.map(l => l.name); if (issueLabels.includes('awaiting-confirmation')) { - console.log('Remediation confirmed. Updating labels.'); + console.log('Remediation confirmed. Generating task checklist...'); + + // Fetch the last bot comment that contains the analysis + const { data: allComments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); + const analysisComment = [...allComments].reverse().find( + c => c.user.login === 'github-actions[bot]' && c.body.includes('Prioritized Remediation Steps') + ); + const priorAnalysis = analysisComment ? analysisComment.body : '(no prior analysis found)'; + + const taskModel = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); + const taskPrompt = `Based on this issue remediation analysis, generate a concise GitHub Markdown task checklist using - [ ] checkboxes. Group tasks under clear headings (e.g., ## Investigation, ## Code Changes, ## Verification). Keep each task to one concrete action. Output only the checklist — no preamble, no summary. + +Analysis: +${truncate(priorAnalysis, 3000)}`; + let taskList = ''; + try { + const taskResult = await fetchWithBackoff(() => taskModel.generateContent(taskPrompt)); + taskList = taskResult.response.text(); + } catch (e) { + console.error("Failed to generate task checklist:", e.message); + taskList = '_(Checklist generation failed — please review the analysis comment above and create tasks manually.)_'; + } + await ensureLabel(octokit, owner, repo, 'remediation-approved', { description: 'Remediation steps approved by maintainer', color: '0e8a16' }); await octokit.rest.issues.addLabels({ owner, repo, issue_number: number, labels: ['remediation-approved'] }); // Remove awaiting-confirmation label const remaining = issueLabels.filter(l => l !== 'awaiting-confirmation'); try { await octokit.rest.issues.removeLabel({ owner, repo, issue_number: number, name: 'awaiting-confirmation' }); } catch {} - await octokit.rest.issues.createComment({ owner, repo, issue_number: number, body: '✅ Remediation confirmed. Automated follow-up actions may proceed (none implemented yet).'}); + await octokit.rest.issues.createComment({ + owner, repo, issue_number: number, + body: `✅ **Remediation Approved — Generated Task Checklist**\n\n${taskList}`, + }); } else { console.log('Confirmation comment received but issue not in awaiting-confirmation state.'); } @@ -658,7 +775,13 @@ async function run() { console.log('Refine analysis requested.'); const issueTitle = context.payload.issue.title; const issueBody = context.payload.issue.body; - await handleNewIssue(octokit, owner, repo, number, issueTitle, issueBody, genAI); // Re-run with fresh model pass + + // Fetch the last bot analysis comment to use as prior context for refinement + const { data: allComments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); + const priorBotComment = [...allComments].reverse().find(c => c.user.login === 'github-actions[bot]'); + const priorAnalysis = priorBotComment ? priorBotComment.body : null; + + await handleNewIssue(octokit, owner, repo, number, issueTitle, issueBody, genAI, priorAnalysis); } else if (commentBody === 'discard recommendations') { console.log('Discard recommendations requested.'); try { await octokit.rest.issues.removeLabel({ owner, repo, issue_number: number, name: 'awaiting-confirmation' }); } catch {} @@ -677,6 +800,28 @@ async function run() { const issueTitle = context.payload.issue.title; const issueBody = context.payload.issue.body; await handleNewIssue(octokit, owner, repo, number, issueTitle, issueBody, genAI); + + } else if (context.eventName === 'issues' && context.payload.action === 'edited') { + // Issue body/title was edited — if no prior analysis exists, run fresh; otherwise nudge for refinement + console.log(`Issue #${number} was edited. Checking for existing analysis...`); + const { data: allComments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); + const hasBotAnalysis = allComments.some( + c => c.user.login === 'github-actions[bot]' && c.body.includes('Prioritized Remediation Steps') + ); + if (hasBotAnalysis) { + // An analysis already exists — notify the author that they can request a refresh + await octokit.rest.issues.createComment({ + owner, repo, issue_number: number, + body: `📝 **Issue Updated** — the description has been edited.\n\nIf the changes significantly affect scope or context, comment \`refine analysis\` and I'll generate a fresh analysis based on the updated content.`, + }); + } else { + // No prior analysis — run fresh analysis + console.log(`No prior analysis found for #${number}. Running fresh analysis.`); + const issueTitle = context.payload.issue.title; + const issueBody = context.payload.issue.body; + await handleNewIssue(octokit, owner, repo, number, issueTitle, issueBody, genAI); + } + } else { console.log(`Event '${context.eventName}' did not match any triggers. No action taken.`); } diff --git a/.github/workflows/gemini_issues_review.yml b/.github/workflows/gemini_issues_review.yml index 4727e9838..5ac47c168 100644 --- a/.github/workflows/gemini_issues_review.yml +++ b/.github/workflows/gemini_issues_review.yml @@ -1,14 +1,17 @@ name: Gemini AI Issue Summarizer on: - # This workflow now triggers when a new issue is opened + # Triggers when a new issue is opened or an existing issue body/title is edited issues: - types: [opened] + types: [opened, edited] jobs: summarize: - # Ensure the job only runs for new issues and not from the bot itself - if: github.event.issue.author.login != 'github-actions[bot]' + # Skip events triggered by the bot itself; for edits, only process body/title changes + if: | + github.actor != 'github-actions[bot]' && + (github.event.action == 'opened' || + (github.event.action == 'edited' && (github.event.changes.body != null || github.event.changes.title != null))) runs-on: ubuntu-latest permissions: contents: read @@ -16,13 +19,21 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' + - name: Cache node modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-gemini-issues-${{ hashFiles('.github/scripts/review.js') }} + restore-keys: | + ${{ runner.os }}-gemini-issues- + - name: Install dependencies run: npm install @actions/github @google/generative-ai @octokit/core