Skip to content

Commit 2c7e88c

Browse files
committed
refactor: enhance pull request summary generation and update workflow service to utilize commit messages
1 parent e2d8ce8 commit 2c7e88c

9 files changed

Lines changed: 168 additions & 295 deletions

File tree

src/core/workflow-service.ts

Lines changed: 24 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
22
import { ActionConfig, PullRequestInfo } from '../types/index.js';
33
import { GitHubService } from '../github/github-service.js';
44
import { LlmService } from '../modules/llm/llm.service.js';
5-
import { CodeReviewResponse } from '../modules/llm/entities/index.js';
5+
import { PullRequestSummaryResponse } from '../modules/llm/entities/index.js';
66
import { LlmRepository } from '../modules/llm/llm.repository.js';
77

88
export class WorkflowService {
@@ -31,10 +31,20 @@ export class WorkflowService {
3131
const prWithContents = await this.githubService.loadFileContents(pullRequestInfo);
3232
core.info(`Loaded content for ${prWithContents.files.length} changed files`);
3333

34-
const reviewResult = await this.llmService.reviewCode(prWithContents);
34+
const commitMessages = await this.githubService.getCommitMessages(owner, repo, prNumber);
3535

36-
core.info('Posting review results...');
37-
await this.postReviewComment(prWithContents, reviewResult);
36+
core.info(`Loaded ${commitMessages.length} commit messages`);
37+
38+
core.info('Generating PR summary...');
39+
const outputLanguage = this.config.llm.outputLanguage || 'en';
40+
const summaryResult = await this.llmService.summarizePullRequest(
41+
prWithContents,
42+
outputLanguage,
43+
commitMessages
44+
);
45+
46+
core.info('Posting summary comment...');
47+
await this.postSummaryComment(prWithContents, summaryResult);
3848

3949
core.info('Code review completed successfully');
4050
} catch (error) {
@@ -43,102 +53,25 @@ export class WorkflowService {
4353
}
4454
}
4555

46-
private async postReviewComment(pullRequest: PullRequestInfo, reviewResult: CodeReviewResponse): Promise<void> {
47-
let comment = this.buildReviewComment(reviewResult);
48-
49-
if (this.config.llm.outputLanguage && this.config.llm.outputLanguage !== 'en') {
50-
comment = await this.llmService.translateText(comment, this.config.llm.outputLanguage);
51-
}
52-
56+
private async postSummaryComment(
57+
pullRequest: PullRequestInfo,
58+
summaryResult: PullRequestSummaryResponse
59+
): Promise<void> {
60+
const comment = this.buildSummaryComment(summaryResult);
5361

5462
await this.githubService.createReview(
5563
pullRequest.owner,
5664
pullRequest.repo,
5765
pullRequest.prNumber,
5866
pullRequest.headSha,
59-
comment,
67+
comment
6068
);
6169
}
6270

63-
private buildReviewComment(reviewResult: CodeReviewResponse): string {
64-
const summary = this.buildSummarySection(reviewResult);
65-
const suggestions = this.buildSuggestionsSection(reviewResult);
66-
const issues = this.buildIssuesSection(reviewResult);
67-
const watermark = '\n\n---\n*Reviewed by flash* ✨';
68-
69-
return `${summary}\n\n${suggestions}\n\n${issues}\n${watermark}`;
70-
}
71-
72-
private buildSummarySection(reviewResult: CodeReviewResponse): string {
73-
return `# Flash Review \n\n${reviewResult.summary}\n\n`;
74-
}
75-
76-
private buildSuggestionsSection(reviewResult: CodeReviewResponse): string {
77-
const sections = [];
78-
79-
if (reviewResult.suggestions.critical.length > 0) {
80-
sections.push(
81-
'## Critical Issues 🚨\n' +
82-
reviewResult.suggestions.critical
83-
.map(
84-
(suggestion) =>
85-
`- **${suggestion.category}** (${suggestion.file}:${suggestion.location}):\n ${suggestion.description}`
86-
)
87-
.join('\n')
88-
);
89-
}
90-
91-
if (reviewResult.suggestions.important.length > 0) {
92-
sections.push(
93-
'## Important Improvements ⚠️\n' +
94-
reviewResult.suggestions.important
95-
.map(
96-
(suggestion) =>
97-
`- **${suggestion.category}** (${suggestion.file}:${suggestion.location}):\n ${suggestion.description}`
98-
)
99-
.join('\n')
100-
);
101-
}
102-
103-
return sections.length > 0 ? sections.join('\n\n') : '';
104-
}
105-
106-
private buildIssuesSection(reviewResult: CodeReviewResponse): string {
107-
const sections = [];
108-
109-
if (reviewResult.issues.security.length > 0) {
110-
sections.push(
111-
`## Security Issues\n${reviewResult.issues.security.map((issue: string) => `- ${issue}`).join('\n')}`
112-
);
113-
}
114-
115-
if (reviewResult.issues.performance.length > 0) {
116-
sections.push(
117-
`## Performance Issues\n${reviewResult.issues.performance.map((issue: string) => `- ${issue}`).join('\n')}`
118-
);
119-
}
120-
121-
if (reviewResult.issues.typescript.length > 0) {
122-
sections.push(
123-
`## TypeScript Issues\n${reviewResult.issues.typescript.map((issue: string) => `- ${issue}`).join('\n')}`
124-
);
125-
}
126-
127-
return sections.length > 0 ? `\n\n${sections.join('\n\n')}` : '';
128-
}
129-
130-
private buildTokenUsageSection(reviewResult: CodeReviewResponse): void {
131-
if (!reviewResult.usageMetadata) {
132-
return;
133-
}
134-
135-
core.info(`## Token Usage
71+
private buildSummaryComment(summaryResult: PullRequestSummaryResponse): string {
72+
const header = '# ✨ Flash Code Review';
73+
const summary = `\n\n${summaryResult.summary}`;
13674

137-
| Model | Prompt Tokens | Completion Tokens | Total Tokens |
138-
|-------|--------------|-------------------|--------------|
139-
| ${this.config.llm.model} | ${reviewResult.usageMetadata.promptTokens}
140-
| ${reviewResult.usageMetadata.completionTokens}
141-
| ${reviewResult.usageMetadata.totalTokens} |`
142-
)
75+
return `${header}${summary}`;
14376
}
14477
}

src/github/github-service.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,23 +129,29 @@ export class GitHubService {
129129
async loadFileContents(pullRequestInfo: PullRequestInfo): Promise<PullRequestInfo> {
130130
const { owner, repo, baseBranch, headBranch, files } = pullRequestInfo;
131131

132+
const MAX_ADDITIONS_FOR_FULL_CONTENT = 800;
133+
132134
const filesWithContent = await Promise.all(
133135
files.map(async (file) => {
134-
if (file.status !== 'removed') {
135-
const branchToUse = file.status === 'added' ? headBranch : baseBranch;
136-
let content = await this.getFileContent(owner, repo, file.filename, branchToUse);
137-
138-
if (!content && file.status === 'modified') {
139-
content = await this.getFileContent(owner, repo, file.filename, baseBranch);
140-
}
141-
142-
return {
143-
...file,
144-
contents: content || undefined,
145-
};
136+
if (file.status === 'removed') {
137+
return file;
138+
}
139+
140+
if (file.status === 'added' && file.additions > MAX_ADDITIONS_FOR_FULL_CONTENT) {
141+
return file;
146142
}
147143

148-
return file;
144+
const branchToUse = file.status === 'added' ? headBranch : baseBranch;
145+
let content = await this.getFileContent(owner, repo, file.filename, branchToUse);
146+
147+
if (!content && file.status === 'modified') {
148+
content = await this.getFileContent(owner, repo, file.filename, baseBranch);
149+
}
150+
151+
return {
152+
...file,
153+
contents: content || undefined,
154+
};
149155
})
150156
);
151157

@@ -259,6 +265,17 @@ export class GitHubService {
259265
}
260266
}
261267
}
268+
269+
async getCommitMessages(owner: string, repo: string, prNumber: number, limit: number = 10): Promise<string[]> {
270+
const { data: commits } = await this.octokit.pulls.listCommits({
271+
owner,
272+
repo,
273+
pull_number: prNumber,
274+
per_page: limit,
275+
});
276+
277+
return commits.slice(0, limit).map((commit) => commit.commit.message.split('\n')[0]);
278+
}
262279
}
263280

264281
interface RepoItem {

src/modules/llm/entities/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './llm.entity.js';
22
export * from './gemini.entity.js';
3+
export * from './pr-summary.entity.js';
34

45
export interface LlmResponse {
56
content: string;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { TokenUsage } from './index.js';
2+
3+
export interface PullRequestSummaryResponse {
4+
summary: string;
5+
usageMetadata: TokenUsage;
6+
}

src/modules/llm/llm.repository.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import { LlmConfig, LlmResponse, GeminiResponse } from './entities/index.js';
33
import { LlmMapper } from './mappers/llm.mapper.js';
44

55
export class LlmRepository {
6-
constructor(private readonly config: LlmConfig) { }
6+
constructor(private readonly config: LlmConfig) {}
77

88
private mapper = LlmMapper;
99

10-
async generateContent(prompt: Array<{ text: string }>, returnJSON: boolean = true): Promise<LlmResponse> {
10+
async generateContent(
11+
prompt: Array<{ text: string }>,
12+
returnJSON: boolean = true,
13+
systemInstruction?: string
14+
): Promise<LlmResponse> {
1115
core.info('Starting Gemini Service');
1216

1317
if (!this.config?.apiKey) {
@@ -16,31 +20,29 @@ export class LlmRepository {
1620

1721
const model = this.config?.model || 'gemini-2.5-flash';
1822
const endpoint = this.mapper.buildGeminiEndpoint(model);
19-
try {
20-
const response = await this.executeRequest(endpoint, prompt, returnJSON);
21-
22-
if (!response.ok) {
23-
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
24-
}
23+
const response = await this.executeRequest(endpoint, prompt, returnJSON, systemInstruction);
24+
if (!response.ok) {
25+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
26+
}
2527

26-
const data = (await response.json()) as GeminiResponse;
27-
core.info(JSON.stringify(data));
28+
const data = (await response.json()) as GeminiResponse;
2829

29-
core.info(
30-
`Tokens used: ${data?.usageMetadata?.promptTokenCount} prompt,
31-
${data?.usageMetadata?.candidatesTokenCount} completion,
32-
${data?.usageMetadata?.totalTokenCount} total`
33-
);
30+
core.info(
31+
`Tokens used: ${data?.usageMetadata?.promptTokenCount} prompt,
32+
${data?.usageMetadata?.candidatesTokenCount} completion,
33+
${data?.usageMetadata?.totalTokenCount} total`
34+
);
3435

35-
return this.mapper.mapGeminiResponse(data, model);
36-
} catch (error) {
37-
core.error(`Error generating content: ${error}`);
38-
throw error;
39-
}
36+
return this.mapper.mapGeminiResponse(data, model);
4037
}
4138

42-
private async executeRequest(endpoint: string, prompt: Array<{ text: string }>, returnJSON = true): Promise<Response> {
43-
const systemInstruction = this.mapper.getSystemInstruction();
39+
private async executeRequest(
40+
endpoint: string,
41+
prompt: Array<{ text: string }>,
42+
returnJSON = true,
43+
customSystemInstruction?: string
44+
): Promise<Response> {
45+
const systemInstruction = customSystemInstruction || this.mapper.getSystemInstruction();
4446

4547
return fetch(endpoint, {
4648
method: 'POST',

src/modules/llm/llm.service.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
import { PullRequestInfo } from '../../types/index.js';
22
import { LlmMapper } from './mappers/llm.mapper.js';
33
import { LlmRepository } from './llm.repository.js';
4-
import { CodeReviewResponse } from './entities/index.js';
4+
import { PullRequestSummaryResponse } from './entities/index.js';
55

66
export class LlmService {
77
constructor(private readonly llmRepository: LlmRepository) {}
88

9-
async reviewCode(pullRequest: PullRequestInfo): Promise<CodeReviewResponse> {
10-
const prompt = LlmMapper.buildReviewPrompt(pullRequest);
11-
const response = await this.llmRepository.generateContent(prompt, true);
9+
async summarizePullRequest(
10+
pullRequest: PullRequestInfo,
11+
outputLanguage: string = 'en',
12+
commitMessages: string[] = []
13+
): Promise<PullRequestSummaryResponse> {
14+
const prompt = LlmMapper.buildSummaryPrompt(pullRequest, commitMessages);
15+
const systemInstruction = LlmMapper.getSummarySystemInstruction(outputLanguage);
16+
const response = await this.llmRepository.generateContent(prompt, false, systemInstruction);
1217

13-
return LlmMapper.parseReviewResponse(response);
14-
}
15-
16-
async translateText(content: string, targetLanguage: string): Promise<string> {
17-
if (targetLanguage.toLowerCase() === 'en') {
18-
return content;
19-
}
20-
21-
const prompt = LlmMapper.buildTranslationPrompt(content, targetLanguage);
22-
const response = await this.llmRepository.generateContent([{ text: prompt }], false);
23-
24-
return response.content;
18+
return LlmMapper.parseSummaryResponse(response);
2519
}
2620
}

0 commit comments

Comments
 (0)