Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/auto-pr-description/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ runs:
printf '%s\n' "$DESCRIPTION" > pr_body.md
fi

cat pr_body.md

# Add JIRA ticket link if found
PR_TITLE=$(gh pr view ${{ inputs.pr-number }} --json title --jq '.title')
TICKET_ID=$(echo "$PR_TITLE" | grep -oE '[A-Z]+-[0-9]+' || true)
Expand Down
103 changes: 62 additions & 41 deletions .github/actions/auto-pr-description/generate_pr_description.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ const fs = require('fs');
const path = require('path');

// Configuration constants
const MAX_TOKENS_PER_REQUEST = 100000; // Conservative limit for Gemini 2.5 Flash
const MAX_TOKENS_PER_REQUEST = 80000; // Conservative limit for Gemini 2.5 Flash
const CHARS_PER_TOKEN = 4; // Rough estimation
const MAX_CHARS_PER_CHUNK = MAX_TOKENS_PER_REQUEST * CHARS_PER_TOKEN;
const MAX_CHUNKS = 10; // Limit to prevent excessive API calls
//const MAX_CHARS_PER_CHUNK = MAX_TOKENS_PER_REQUEST * CHARS_PER_TOKEN;
const MAX_CHUNKS = 3; // Limit to prevent excessive API calls

/**
* Estimate token count for text (rough approximation)
Expand All @@ -16,35 +16,80 @@ function estimateTokens(text) {
return Math.ceil(text.length / CHARS_PER_TOKEN);
}


function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}


function splitStringByTokens(str, maxTokens) {
console.error('splitStringByTokens');
const words = str.split(' ');
const result = [];
let currentLine = '';

for (const word of words) {
if (estimateTokens(currentLine + word) <= maxTokens) {
currentLine += (currentLine ? ' ' : '') + word;
} else {
if (currentLine) result.push(currentLine);
currentLine = word;
}
}

if (currentLine) result.push(currentLine);

return result;
}


/**
* Split diff into chunks by file boundaries
*/
function chunkDiffByFiles(diffContent) {
console.error('chunkDiffByFiles');
const fileChunks = [];
const lines = diffContent.split('\n');
let currentChunk = '';
let currentFile = '';
let tokenCount = 0;

for (const line of lines) {
// Check if this is a new file header
//console.error(`Line is estimated at ${estimateTokens(line)} tokens`);
tokenCount += estimateTokens(line);
//console.error(`Total tokens for this chunk is ${tokenCount}`);
if (line.startsWith('diff --git') || line.startsWith('+++') || line.startsWith('---')) {
// If we have content and it's getting large, save current chunk
if (currentChunk && estimateTokens(currentChunk) > MAX_CHARS_PER_CHUNK / 2) {
if (currentChunk && tokenCount > MAX_TOKENS_PER_REQUEST) {
fileChunks.push({
content: currentChunk.trim(),
file: currentFile,
type: 'file-chunk'
});
currentChunk = '';
tokenCount = 0;
}

// Start new chunk
currentChunk = line + '\n';


// Extract filename for reference
if (line.startsWith('+++')) {
currentFile = line.replace('+++ b/', '').replace('+++ a/', '');
}
if(tokenCount > MAX_TOKENS_PER_REQUEST){
const split_chunk = splitStringByTokens(currentChunk, MAX_TOKENS_PER_REQUEST);
currentChunk = split_chunk[split_chunk.length-1];
for(let i = 0; i < split_chunk.length -1;i++){
fileChunks.push({
content: split_chunk[i].trim(),
file: currentFile,
type: 'file-chunk'
});
}
}
} else {
currentChunk += line + '\n';
}
Expand Down Expand Up @@ -107,7 +152,8 @@ ${diffContent}`;
* Call Gemini API with the given prompt
*/
async function callGeminiAPI(prompt, apiKey) {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, {
console.error(`Sending prompt with an estimated ${estimateTokens(prompt)} tokens`);
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Expand Down Expand Up @@ -147,6 +193,7 @@ async function callGeminiAPI(prompt, apiKey) {
* Process diff chunks and combine results
*/
async function processChunks(chunks, apiKey) {
console.error('processchunks');
if (chunks.length === 1) {
// Single chunk, process normally
return await callGeminiAPI(createPRPrompt(chunks[0].content), apiKey);
Expand All @@ -157,6 +204,10 @@ async function processChunks(chunks, apiKey) {

for (let i = 0; i < Math.min(chunks.length, MAX_CHUNKS); i++) {
const chunk = chunks[i];
if (i > 0) {
// sleep for 3 seconds
sleep(5 * 1000);
}
console.error(`Processing chunk ${i + 1}/${Math.min(chunks.length, MAX_CHUNKS)} (${chunk.file || 'unknown file'})`);

try {
Expand All @@ -174,7 +225,7 @@ async function processChunks(chunks, apiKey) {
if (chunkResults.length === 0) {
throw new Error('Failed to process any chunks');
}

sleep(5*1000);
// Combine results from multiple chunks
const combinedPrompt = `Combine these pull request descriptions into a single, coherent PR description. Use the same format:

Expand Down Expand Up @@ -215,42 +266,12 @@ Create a unified description that captures the overall changes across all files.
if (estimatedTokens > MAX_TOKENS_PER_REQUEST) {
console.error('Large diff detected, using chunking strategy...');

// For extremely large diffs, first try to summarize
if (estimatedTokens > MAX_TOKENS_PER_REQUEST * 5) {
console.error('Extremely large diff detected, using summary approach...');
const summaryPrompt = createSummaryPrompt(diffContent);
result = await callGeminiAPI(summaryPrompt, apiKey);

// Create a simplified PR description based on the summary
const prPrompt = `Based on this summary of changes, create a pull request description using this format:

## Description
Brief summary of changes (1-2 sentences max).

## Changes
- [ ] Key change 1
- [ ] Key change 2
- [ ] Key change 3 (max 5 items)

## Verification
- [ ] Test step 1
- [ ] Test step 2
- [ ] Test step 3 (max 3 items)

Summary: ${result}`;

result = await callGeminiAPI(prPrompt, apiKey);
} else {
// Chunk the diff and process
const chunks = chunkDiffByFiles(diffContent);
console.error(`Split diff into ${chunks.length} chunks`);

if (chunks.length > MAX_CHUNKS) {
console.error(`Warning: Too many chunks (${chunks.length}), processing first ${MAX_CHUNKS} chunks only`);
}

result = await processChunks(chunks, apiKey);
const chunks = chunkDiffByFiles(diffContent);
console.error(`Split diff into ${chunks.length} chunks`);
if (chunks.length > MAX_CHUNKS) {
console.error(`Warning: Too many chunks (${chunks.length}), processing first ${MAX_CHUNKS} chunks only`);
}
result = await processChunks(chunks, apiKey);
} else {
// Small diff, process normally
result = await callGeminiAPI(createPRPrompt(diffContent), apiKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Keep it concise and focused on the most important changes.`;
const combinedPrompt = `${promptTemplate}\n\nHere is the git diff:\n\n${diffContent}`;

try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-actions",
"version": "2.24.0",
"version": "2.25.1",
"description": "Used to store GitHub actions for use across the enterprise",
"scripts": {
"test": "./tooling/scripts/run_tests.sh",
Expand Down