Skip to content

Commit 09ff67b

Browse files
feat: 🎸 update the release description code
✅ Closes: CORE-2803
1 parent b0b59b7 commit 09ff67b

2 files changed

Lines changed: 263 additions & 68 deletions

File tree

‎.github/actions/auto-release-description/action.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ runs:
8080
printf '%s\n' "$DESCRIPTION" > pr_body.md
8181
fi
8282
83+
cat pr_body.md
84+
8385
# Add JIRA ticket link if found
8486
PR_TITLE=$(gh pr view ${{ inputs.pr-number }} --json title --jq '.title')
8587
TICKET_ID=$(echo "$PR_TITLE" | grep -oE '[A-Z]+-[0-9]+' || true)

‎.github/actions/auto-release-description/generate_pr_description.js‎

Lines changed: 261 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,251 @@
33
const fs = require('fs');
44
const path = require('path');
55

6+
// Configuration constants
7+
const MAX_TOKENS_PER_REQUEST = 80000; // Conservative limit for Gemini 2.5 Flash
8+
const CHARS_PER_TOKEN = 4; // Rough estimation
9+
//const MAX_CHARS_PER_CHUNK = MAX_TOKENS_PER_REQUEST * CHARS_PER_TOKEN;
10+
const MAX_CHUNKS = 3; // Limit to prevent excessive API calls
11+
12+
/**
13+
* Estimate token count for text (rough approximation)
14+
*/
15+
function estimateTokens(text) {
16+
return Math.ceil(text.length / CHARS_PER_TOKEN);
17+
}
18+
19+
20+
function sleep(ms) {
21+
return new Promise(resolve => setTimeout(resolve, ms));
22+
}
23+
24+
25+
function splitStringByTokens(str, maxTokens) {
26+
console.error('splitStringByTokens');
27+
const words = str.split(' ');
28+
const result = [];
29+
let currentLine = '';
30+
31+
for (const word of words) {
32+
if (estimateTokens(currentLine + word) <= maxTokens) {
33+
currentLine += (currentLine ? ' ' : '') + word;
34+
} else {
35+
if (currentLine) result.push(currentLine);
36+
currentLine = word;
37+
}
38+
}
39+
40+
if (currentLine) result.push(currentLine);
41+
42+
return result;
43+
}
44+
45+
46+
/**
47+
* Split diff into chunks by file boundaries
48+
*/
49+
function chunkDiffByFiles(diffContent) {
50+
console.error('chunkDiffByFiles');
51+
const fileChunks = [];
52+
const lines = diffContent.split('\n');
53+
let currentChunk = '';
54+
let currentFile = '';
55+
let tokenCount = 0;
56+
57+
for (const line of lines) {
58+
// Check if this is a new file header
59+
//console.error(`Line is estimated at ${estimateTokens(line)} tokens`);
60+
tokenCount += estimateTokens(line);
61+
//console.error(`Total tokens for this chunk is ${tokenCount}`);
62+
if (line.startsWith('diff --git') || line.startsWith('+++') || line.startsWith('---')) {
63+
// If we have content and it's getting large, save current chunk
64+
if (currentChunk && tokenCount > MAX_TOKENS_PER_REQUEST) {
65+
fileChunks.push({
66+
content: currentChunk.trim(),
67+
file: currentFile,
68+
type: 'file-chunk'
69+
});
70+
currentChunk = '';
71+
tokenCount = 0;
72+
}
73+
74+
// Start new chunk
75+
currentChunk = line + '\n';
76+
77+
78+
// Extract filename for reference
79+
if (line.startsWith('+++')) {
80+
currentFile = line.replace('+++ b/', '').replace('+++ a/', '');
81+
}
82+
if(tokenCount > MAX_TOKENS_PER_REQUEST){
83+
const split_chunk = splitStringByTokens(currentChunk, MAX_TOKENS_PER_REQUEST);
84+
currentChunk = split_chunk[split_chunk.length-1];
85+
for(let i = 0; i < split_chunk.length -1;i++){
86+
fileChunks.push({
87+
content: split_chunk[i].trim(),
88+
file: currentFile,
89+
type: 'file-chunk'
90+
});
91+
}
92+
}
93+
} else {
94+
currentChunk += line + '\n';
95+
}
96+
}
97+
98+
// Add the last chunk
99+
if (currentChunk.trim()) {
100+
fileChunks.push({
101+
content: currentChunk.trim(),
102+
file: currentFile,
103+
type: 'file-chunk'
104+
});
105+
}
106+
107+
return fileChunks;
108+
}
109+
110+
/**
111+
* Create a summary prompt for extremely large diffs
112+
*/
113+
function createSummaryPrompt(diffContent) {
114+
return `Analyze this git diff and provide a high-level summary. Focus on:
115+
1. What types of files were changed (e.g., source code, tests, config, docs)
116+
2. The overall scope of changes (e.g., new feature, bug fix, refactor)
117+
3. Any major architectural changes or new dependencies
118+
119+
Keep the summary to 2-3 sentences maximum.
120+
121+
Git diff:
122+
${diffContent}`;
123+
}
124+
125+
/**
126+
* Create the main PR description prompt
127+
*/
128+
function createPRPrompt(diffContent) {
129+
return `Write a release note summary with the following sections:
130+
131+
## Infrastructure Changes
132+
Highlight changes to AWS resources, IaC (CloudFormation/SAM templates), Lambda functions, databases, and deployment configurations.
133+
134+
## Security Concerns
135+
Identify any security-related changes, authentication/authorization updates, data access modifications, or potential vulnerabilities.
136+
137+
## Performance Implications
138+
Assess any changes that could impact system performance, database queries, API response times, or resource consumption.
139+
140+
## New Features
141+
Describe new functionality, enhancements, or capabilities being introduced.
142+
143+
## Student Access Risk Analysis
144+
Evaluate the risk that these changes could cause students to lose access to their course materials. Assess changes to:
145+
146+
- Term ID handling and relationships
147+
- Adoption, section, product, or term product relationships
148+
- Data export or deduplication logic
149+
- Database schema affecting terms, sections, or student access
150+
- Section Channel Offer (SCO) activation/deactivation logic
151+
- Query logic for student materials or permissions
152+
153+
For each risk identified, provide: Risk Level (NONE/LOW/MEDIUM/HIGH/CRITICAL), specific changes, risk description, and recommended verification steps.
154+
155+
If no student access risks are detected, state: "Risk Level: NONE - No changes affect student material access."
156+
157+
Print only the report and ask no questions.
158+
159+
${diffContent}`;
160+
}
161+
162+
/**
163+
* Call Gemini API with the given prompt
164+
*/
165+
async function callGeminiAPI(prompt, apiKey) {
166+
console.error(`Sending prompt with an estimated ${estimateTokens(prompt)} tokens`);
167+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
168+
method: 'POST',
169+
headers: { 'Content-Type': 'application/json' },
170+
body: JSON.stringify({
171+
contents: [{
172+
parts: [{
173+
text: prompt
174+
}]
175+
}],
176+
generationConfig: {
177+
temperature: 0.7,
178+
topK: 40,
179+
topP: 0.95,
180+
maxOutputTokens: 8192,
181+
}
182+
})
183+
});
184+
185+
if (!response.ok) {
186+
const errorText = await response.text();
187+
throw new Error(`Gemini API request failed with status ${response.status}: ${errorText}`);
188+
}
189+
190+
const json = await response.json();
191+
192+
if (!json.candidates || !json.candidates[0]) {
193+
throw new Error('Invalid response from Gemini API');
194+
}
195+
196+
if (!json.candidates[0].content || !json.candidates[0].content.parts || !json.candidates[0].content.parts[0] || !json.candidates[0].content.parts[0].text) {
197+
throw new Error('Invalid response structure from Gemini API - missing content');
198+
}
199+
200+
return json.candidates[0].content.parts[0].text;
201+
}
202+
203+
/**
204+
* Process diff chunks and combine results
205+
*/
206+
async function processChunks(chunks, apiKey) {
207+
console.error('processchunks');
208+
if (chunks.length === 1) {
209+
// Single chunk, process normally
210+
return await callGeminiAPI(createPRPrompt(chunks[0].content), apiKey);
211+
}
212+
213+
// Multiple chunks - process each and combine
214+
const chunkResults = [];
215+
216+
for (let i = 0; i < Math.min(chunks.length, MAX_CHUNKS); i++) {
217+
const chunk = chunks[i];
218+
if (i > 0) {
219+
// sleep for 3 seconds
220+
sleep(5 * 1000);
221+
}
222+
console.error(`Processing chunk ${i + 1}/${Math.min(chunks.length, MAX_CHUNKS)} (${chunk.file || 'unknown file'})`);
223+
224+
try {
225+
const result = await callGeminiAPI(createPRPrompt(chunk.content), apiKey);
226+
chunkResults.push({
227+
file: chunk.file,
228+
result: result
229+
});
230+
} catch (error) {
231+
console.error(`Warning: Failed to process chunk ${i + 1}: ${error.message}`);
232+
// Continue with other chunks
233+
}
234+
}
235+
236+
if (chunkResults.length === 0) {
237+
throw new Error('Failed to process any chunks');
238+
}
239+
sleep(5*1000);
240+
// Combine results from multiple chunks
241+
const combinedPrompt = `Combine these pull request descriptions into a single, coherent PR description. Use the same format:
242+
243+
${chunkResults.map((chunk, index) => `## Chunk ${index + 1} (${chunk.file}):
244+
${chunk.result}`).join('\n\n')}
245+
246+
Create a unified description that captures the overall changes across all files.`;
247+
248+
return await callGeminiAPI(combinedPrompt, apiKey);
249+
}
250+
6251
(async () => {
7252
const [, , diffFile] = process.argv;
8253
if (!diffFile) {
@@ -21,80 +266,28 @@ const path = require('path');
21266
process.exit(1);
22267
}
23268

24-
// Create prompt for PR description generation
25-
const promptTemplate = `Write a concise pull request description based on the git diff. Use this exact format:
26-
27-
## Description
28-
Brief summary of changes (1-2 sentences max).
29-
30-
## Changes
31-
- [ ] Key change 1
32-
- [ ] Key change 2
33-
- [ ] Key change 3 (max 5 items)
34-
35-
## Verification
36-
- [ ] Test step 1
37-
- [ ] Test step 2
38-
- [ ] Test step 3 (max 3 items)
39-
40-
Keep it concise and focused on the most important changes.`;
41-
42269
const diffContent = fs.readFileSync(diffFile, 'utf8');
43-
const combinedPrompt = `${promptTemplate}\n\nHere is the git diff:\n\n${diffContent}`;
270+
const estimatedTokens = estimateTokens(diffContent);
271+
272+
console.error(`Diff size: ${diffContent.length} characters (~${estimatedTokens} tokens)`);
44273

45274
try {
46-
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
47-
method: 'POST',
48-
headers: { 'Content-Type': 'application/json' },
49-
body: JSON.stringify({
50-
contents: [{
51-
parts: [{
52-
text: combinedPrompt
53-
}]
54-
}],
55-
generationConfig: {
56-
temperature: 0.7,
57-
topK: 40,
58-
topP: 0.95,
59-
maxOutputTokens: 8192,
60-
}
61-
})
62-
});
63-
64-
if (!response.ok) {
65-
const errorText = await response.text();
66-
console.error(`Error: Gemini API request failed with status ${response.status}`);
67-
console.error(`Response: ${errorText}`);
68-
process.exit(1);
69-
}
70-
71-
const json = await response.json();
72-
73-
if (!json.candidates || !json.candidates[0]) {
74-
console.error('Error: Invalid response from Gemini API');
75-
console.error(JSON.stringify(json, null, 2));
76-
process.exit(1);
77-
}
78-
79-
// Check if response was truncated due to max tokens
80-
if (json.candidates[0].finishReason === 'MAX_TOKENS') {
81-
console.error('Warning: Response was truncated due to token limit. Consider reducing diff size or using more specific ignore-files.');
82-
// Continue processing the partial response
83-
}
84-
85-
if (!json.candidates[0].content) {
86-
console.error('Error: No content in API response');
87-
console.error(JSON.stringify(json, null, 2));
88-
process.exit(1);
89-
}
275+
let result;
90276

91-
if (!json.candidates[0].content.parts || !json.candidates[0].content.parts[0] || !json.candidates[0].content.parts[0].text) {
92-
console.error('Error: Invalid response structure from Gemini API - missing parts or text');
93-
console.error(JSON.stringify(json, null, 2));
94-
process.exit(1);
277+
if (estimatedTokens > MAX_TOKENS_PER_REQUEST) {
278+
console.error('Large diff detected, using chunking strategy...');
279+
280+
const chunks = chunkDiffByFiles(diffContent);
281+
console.error(`Split diff into ${chunks.length} chunks`);
282+
if (chunks.length > MAX_CHUNKS) {
283+
console.error(`Warning: Too many chunks (${chunks.length}), processing first ${MAX_CHUNKS} chunks only`);
284+
}
285+
result = await processChunks(chunks, apiKey);
286+
} else {
287+
// Small diff, process normally
288+
result = await callGeminiAPI(createPRPrompt(diffContent), apiKey);
95289
}
96290

97-
const result = json.candidates[0].content.parts[0].text;
98291
process.stdout.write(result);
99292
} catch (error) {
100293
console.error(`Error: Failed to generate pull request description: ${error.message}`);

0 commit comments

Comments
 (0)