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
25 changes: 8 additions & 17 deletions scripts/generate-release-notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ export function buildPullRequestQuery(numbers) {
pr${index}: pullRequest(number: ${number}) {
number
body
additions
deletions
changedFiles
labels(first: 20) { nodes { name } }
files(first: 40) { nodes { path } }
}`,
)
.join('\n');
Expand Down Expand Up @@ -262,14 +258,7 @@ function compactEntry(entry) {
return {
number: entry.number,
title: entry.title,
body: (entry.body || '').slice(0, 3000),
labels: (entry.labels || []).map((label) =>
typeof label === 'string' ? label : label.name,
),
files: (entry.files || []).slice(0, 40),
additions: entry.additions,
deletions: entry.deletions,
changedFiles: entry.changedFiles,
body: (entry.body || '').slice(0, 700),
category: classifyChange(entry),
};
}
Expand Down Expand Up @@ -375,15 +364,10 @@ export function enrichEntries(entries, metadata) {
const byNumber = new Map(metadata.map((item) => [item.number, item]));
return entries.map((entry) => {
const details = byNumber.get(entry.number) || {};
const files = details.files?.nodes || details.files || [];
return {
...entry,
body: details.body || '',
labels: details.labels?.nodes || details.labels || [],
files: files.map((file) => (typeof file === 'string' ? file : file.path)),
additions: details.additions || 0,
deletions: details.deletions || 0,
changedFiles: details.changedFiles || files.length,
};
});
}
Expand Down Expand Up @@ -468,6 +452,7 @@ export function createOpenAiCompleter({
if (remainingMs <= 0) {
throw deadlineError();
}
const attemptStartedAt = Date.now();
try {
const response = await fetchImpl(endpoint, {
method: 'POST',
Expand Down Expand Up @@ -495,10 +480,16 @@ export function createOpenAiCompleter({
if (typeof content !== 'string' || !content.trim()) {
throw new Error(CONTENT_VALIDATION_ERROR_MESSAGE);
}
console.error(
`Model ${request.kind} request succeeded in ${Date.now() - attemptStartedAt}ms (prompt ${prompt.user.length} chars).`,
);
return content;
} catch (error) {
lastError = error;
attempt += 1;
console.error(
`Model ${request.kind} request failed after ${Date.now() - attemptStartedAt}ms (prompt ${prompt.user.length} chars): ${escapeWorkflowCommand(error.message)}`,
);
if (Date.now() >= deadline) {
throw deadlineError();
}
Expand Down
43 changes: 32 additions & 11 deletions scripts/tests/generate-release-notes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ const entry = (number, title, labels = []) => ({
author: 'alice',
labels,
body: '',
files: [],
additions: 1,
deletions: 0,
changedFiles: 1,
});

describe('parseGeneratedEntries', () => {
Expand Down Expand Up @@ -240,6 +236,34 @@ describe('generateAiContent', () => {
]);
});

it('sends only title, a bounded body excerpt, and category to the model', async () => {
const long = { ...entry(1, 'feat: long body'), body: 'x'.repeat(5000) };
const calls = [];
const complete = async (request) => {
calls.push(request);
if (request.kind === 'summaries') {
return JSON.stringify({
summaries: request.entries.map((item) => ({
pr: item.number,
summary: 'Summary.',
})),
});
}
return JSON.stringify({ highlights: [] });
};

await generateAiContent([long], complete);

const [payload] = calls[0].entries;
expect(Object.keys(payload).sort()).toEqual([
'body',
'category',
'number',
'title',
]);
expect(payload.body).toHaveLength(700);
});

it('falls back to original titles for an invalid summary batch', async () => {
const entries = [entry(1, 'feat: original'), entry(2, 'fix: original')];
const complete = async (request) => {
Expand Down Expand Up @@ -359,17 +383,13 @@ describe('enrichEntries', () => {
number: 1,
body: 'Why it matters.',
labels: [{ name: 'type/bug' }],
files: [{ path: 'packages/core/a.ts' }],
additions: 3,
deletions: 2,
changedFiles: 1,
},
]);

expect(enriched.map((item) => item.number)).toEqual([2, 1]);
expect(enriched[0].body).toBe('');
expect(enriched[1].body).toBe('Why it matters.');
expect(enriched[1].files).toEqual(['packages/core/a.ts']);
expect(enriched[1].labels).toEqual([{ name: 'type/bug' }]);
});
});

Expand All @@ -379,7 +399,8 @@ describe('buildPullRequestQuery', () => {

expect(query).toContain('pr0: pullRequest(number: 12)');
expect(query).toContain('pr1: pullRequest(number: 8)');
expect(query).toContain('files(first: 40)');
expect(query).toContain('labels(first: 20)');
expect(query).not.toContain('files(first: 40)');
expect(query).not.toContain('pullRequest(number: undefined)');
});
});
Expand Down Expand Up @@ -480,7 +501,7 @@ describe('generateReleaseNotes', () => {
' process.exit(0);',
'}',
"if (args[0] === 'api' && args[1] === 'graphql') {",
" process.stdout.write(JSON.stringify({ data: { repository: { pr0: { number: 1, body: 'Body.', additions: 1, deletions: 0, changedFiles: 1, labels: { nodes: [] }, files: { nodes: [] } } } } }));",
" process.stdout.write(JSON.stringify({ data: { repository: { pr0: { number: 1, body: 'Body.', labels: { nodes: [] } } } } }));",
' process.exit(0);',
'}',
'process.exit(1);',
Expand Down
Loading