Skip to content

Commit 336fb66

Browse files
authored
fix(code-review): log repo size (#3918)
* fix(code-review): log repository size * fix(github): label repo size as mib * fix(gitlab): label repo size as mib * test(github): update repo size unit * test(gitlab): update repo size unit * refactor(review): remove repo guard * refactor(review): inline size lookup
1 parent 39956d8 commit 336fb66

10 files changed

Lines changed: 249 additions & 3 deletions

File tree

apps/web/src/lib/code-reviews/triggers/prepare-review-payload.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ const mockFindKiloReviewComment = jest.fn();
33
const mockFetchPRInlineComments = jest.fn();
44
const mockGetPRHeadCommit = jest.fn();
55
const mockFetchGitHubRootTextFileAtRef = jest.fn();
6+
const mockFetchGitHubRepositorySize = jest.fn();
67
const mockFindKiloReviewNote = jest.fn();
78
const mockFetchMRInlineComments = jest.fn();
89
const mockGetMRHeadCommit = jest.fn();
910
const mockGetMRDiffRefs = jest.fn();
1011
const mockFetchGitLabRootTextFileAtRef = jest.fn();
12+
const mockFetchGitLabRepositorySize = jest.fn();
1113
const mockGetOrCreateProjectAccessToken = jest.fn();
1214
const mockFindPreviousCompletedReview = jest.fn();
1315
const mockUpdateRepositoryReviewInstructionsMetadata = jest.fn();
@@ -23,6 +25,7 @@ jest.mock('@/lib/integrations/platforms/github/adapter', () => ({
2325
fetchPRInlineComments: (...args: unknown[]) => mockFetchPRInlineComments(...args),
2426
getPRHeadCommit: (...args: unknown[]) => mockGetPRHeadCommit(...args),
2527
fetchGitHubRootTextFileAtRef: (...args: unknown[]) => mockFetchGitHubRootTextFileAtRef(...args),
28+
fetchGitHubRepositorySize: (...args: unknown[]) => mockFetchGitHubRepositorySize(...args),
2629
}));
2730

2831
jest.mock('@/lib/integrations/platforms/gitlab/adapter', () => ({
@@ -31,6 +34,7 @@ jest.mock('@/lib/integrations/platforms/gitlab/adapter', () => ({
3134
getMRHeadCommit: (...args: unknown[]) => mockGetMRHeadCommit(...args),
3235
getMRDiffRefs: (...args: unknown[]) => mockGetMRDiffRefs(...args),
3336
fetchGitLabRootTextFileAtRef: (...args: unknown[]) => mockFetchGitLabRootTextFileAtRef(...args),
37+
fetchGitLabRepositorySize: (...args: unknown[]) => mockFetchGitLabRepositorySize(...args),
3438
GitLabProjectAccessTokenPermissionError: class GitLabProjectAccessTokenPermissionError extends Error {},
3539
}));
3640

@@ -156,6 +160,7 @@ describe('prepareReviewPayload', () => {
156160
mockFetchPRInlineComments.mockResolvedValue([]);
157161
mockGetPRHeadCommit.mockResolvedValue('headsha123');
158162
mockFetchGitHubRootTextFileAtRef.mockResolvedValue('# Review policy\n\nFlag only regressions.');
163+
mockFetchGitHubRepositorySize.mockResolvedValue('100 MB');
159164
mockFindKiloReviewNote.mockResolvedValue(null);
160165
mockFetchMRInlineComments.mockResolvedValue([]);
161166
mockGetMRHeadCommit.mockResolvedValue('headsha123');
@@ -165,6 +170,7 @@ describe('prepareReviewPayload', () => {
165170
headSha: 'headsha123',
166171
});
167172
mockFetchGitLabRootTextFileAtRef.mockResolvedValue('# GitLab review policy');
173+
mockFetchGitLabRepositorySize.mockResolvedValue('100 MB');
168174
mockGetOrCreateProjectAccessToken.mockResolvedValue('gitlab-project-token');
169175
mockFindPreviousCompletedReview.mockResolvedValue(null);
170176
mockUpdateRepositoryReviewInstructionsMetadata.mockResolvedValue(undefined);
@@ -184,11 +190,13 @@ describe('prepareReviewPayload', () => {
184190
mockFetchPRInlineComments.mockReset();
185191
mockGetPRHeadCommit.mockReset();
186192
mockFetchGitHubRootTextFileAtRef.mockReset();
193+
mockFetchGitHubRepositorySize.mockReset();
187194
mockFindKiloReviewNote.mockReset();
188195
mockFetchMRInlineComments.mockReset();
189196
mockGetMRHeadCommit.mockReset();
190197
mockGetMRDiffRefs.mockReset();
191198
mockFetchGitLabRootTextFileAtRef.mockReset();
199+
mockFetchGitLabRepositorySize.mockReset();
192200
mockGetOrCreateProjectAccessToken.mockReset();
193201
mockFindPreviousCompletedReview.mockReset();
194202
mockUpdateRepositoryReviewInstructionsMetadata.mockReset();
@@ -209,7 +217,7 @@ describe('prepareReviewPayload', () => {
209217
.values(defineReview(testUser.id, integration.id))
210218
.returning();
211219

212-
await prepareReviewPayload({
220+
const payload = await prepareReviewPayload({
213221
reviewId: review.id,
214222
owner: { type: 'user', id: testUser.id, userId: testUser.id },
215223
agentConfig: { config: baseAgentConfig },
@@ -223,6 +231,12 @@ describe('prepareReviewPayload', () => {
223231
path: 'REVIEW.md',
224232
ref: 'main',
225233
});
234+
expect(mockFetchGitHubRepositorySize).toHaveBeenCalledWith({
235+
token: 'github-token',
236+
owner: 'test-org',
237+
repo: REPO.split('/')[1],
238+
});
239+
expect(payload.repositorySize).toBe('100 MB');
226240
expect(mockGenerateReviewPrompt).toHaveBeenCalledWith(
227241
expect.any(Object),
228242
REPO,
@@ -268,7 +282,13 @@ describe('prepareReviewPayload', () => {
268282
gitToken: 'gitlab-project-token',
269283
platform: 'gitlab',
270284
});
285+
expect(payload.repositorySize).toBe('100 MB');
271286
expect(payload.sessionInput).not.toHaveProperty('gitlabCodeReviewTokenRef');
287+
expect(mockFetchGitLabRepositorySize).toHaveBeenCalledWith(
288+
'gitlab-project-token',
289+
REPO,
290+
'https://gitlab.example.com'
291+
);
272292
expect(mockFindPreviousCompletedReview).toHaveBeenCalledWith(REPO, 123, 'headsha123', {
273293
platform: 'gitlab',
274294
integrationId: gitlabIntegration.id,
@@ -389,6 +409,29 @@ describe('prepareReviewPayload', () => {
389409
});
390410
});
391411

412+
it('continues payload preparation when repository size lookup fails', async () => {
413+
const [review] = await db
414+
.insert(cloud_agent_code_reviews)
415+
.values(defineReview(testUser.id, integration.id))
416+
.returning();
417+
mockFetchGitHubRepositorySize.mockRejectedValueOnce(new Error('metadata unavailable'));
418+
419+
const payload = await prepareReviewPayload({
420+
reviewId: review.id,
421+
owner: { type: 'user', id: testUser.id, userId: testUser.id },
422+
agentConfig: { config: baseAgentConfig },
423+
platform: 'github',
424+
});
425+
426+
expect(payload.repositorySize).toBeNull();
427+
expect(mockGenerateReviewPrompt).toHaveBeenCalledWith(
428+
expect.any(Object),
429+
REPO,
430+
123,
431+
expect.any(Object)
432+
);
433+
});
434+
392435
it('falls back to built-in guidance when REVIEW.md is empty', async () => {
393436
const [review] = await db
394437
.insert(cloud_agent_code_reviews)

apps/web/src/lib/code-reviews/triggers/prepare-review-payload.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
fetchPRInlineComments,
1919
getPRHeadCommit,
2020
fetchGitHubRootTextFileAtRef,
21+
fetchGitHubRepositorySize,
2122
} from '@/lib/integrations/platforms/github/adapter';
2223
import type { GitHubAppType } from '@/lib/integrations/platforms/github/app-selector';
2324
import {
@@ -27,6 +28,7 @@ import {
2728
getMRDiffRefs,
2829
GitLabProjectAccessTokenPermissionError,
2930
fetchGitLabRootTextFileAtRef,
31+
fetchGitLabRepositorySize,
3032
} from '@/lib/integrations/platforms/gitlab/adapter';
3133
import {
3234
getOrCreateProjectAccessToken,
@@ -101,6 +103,8 @@ export type CodeReviewPayload = {
101103
agentVersion?: string;
102104
/** Cloud-agent session ID from a previous completed review, for session continuation */
103105
previousCloudAgentSessionId?: string;
106+
/** Provider-reported repository storage size, formatted for log correlation. */
107+
repositorySize?: string | null;
104108
};
105109

106110
/**
@@ -158,6 +162,7 @@ export async function prepareReviewPayload(
158162
let existingReviewState: ExistingReviewState | null = null;
159163
let gitlabContext: GitLabDiffContext | undefined;
160164
let repositoryReviewInstructionsLookup = unusedRepositoryReviewInstructionsLookup();
165+
let repositorySize: string | null = null;
161166

162167
if (review.platform_integration_id) {
163168
const integration = await getIntegrationById(review.platform_integration_id);
@@ -175,6 +180,26 @@ export async function prepareReviewPayload(
175180
githubToken = installationToken;
176181
const [repoOwner, repoName] = review.repo_full_name.split('/');
177182

183+
try {
184+
repositorySize = await fetchGitHubRepositorySize({
185+
token: installationToken,
186+
owner: repoOwner,
187+
repo: repoName,
188+
});
189+
logExceptInTest('[prepareReviewPayload] Repository size lookup complete', {
190+
platform,
191+
repoFullName: review.repo_full_name,
192+
repositorySize,
193+
repositorySizeKnown: repositorySize !== null,
194+
});
195+
} catch (error) {
196+
warnExceptInTest('[prepareReviewPayload] Repository size lookup failed; continuing', {
197+
platform,
198+
repoFullName: review.repo_full_name,
199+
error: getReviewInstructionsFetchErrorMetadata(error),
200+
});
201+
}
202+
178203
const repositoryReviewInstructionsPromise =
179204
shouldUseReviewMd && repoOwner && repoName
180205
? fetchRepositoryReviewInstructions({
@@ -289,6 +314,26 @@ export async function prepareReviewPayload(
289314
}
290315
const projectAccessToken = gitlabToken;
291316

317+
try {
318+
repositorySize = await fetchGitLabRepositorySize(
319+
projectAccessToken,
320+
review.repo_full_name,
321+
instanceUrl
322+
);
323+
logExceptInTest('[prepareReviewPayload] Repository size lookup complete', {
324+
platform,
325+
repoFullName: review.repo_full_name,
326+
repositorySize,
327+
repositorySizeKnown: repositorySize !== null,
328+
});
329+
} catch (error) {
330+
warnExceptInTest('[prepareReviewPayload] Repository size lookup failed; continuing', {
331+
platform,
332+
repoFullName: review.repo_full_name,
333+
error: getReviewInstructionsFetchErrorMetadata(error),
334+
});
335+
}
336+
292337
const repositoryReviewInstructionsPromise = shouldUseReviewMd
293338
? fetchRepositoryReviewInstructions({
294339
platform,
@@ -523,12 +568,14 @@ export async function prepareReviewPayload(
523568
sessionInput,
524569
owner,
525570
previousCloudAgentSessionId,
571+
repositorySize,
526572
};
527573

528574
logExceptInTest('[prepareReviewPayload] Prepared payload', {
529575
reviewId,
530576
platform,
531577
owner,
578+
repositorySize,
532579
sessionInput: {
533580
...sessionInput,
534581
githubToken: sessionInput.githubToken ? '***' : undefined, // Redact token

apps/web/src/lib/integrations/platforms/github/adapter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,22 @@ export async function fetchGitHubRepositoryDefaultBranch(params: {
711711
return data.default_branch;
712712
}
713713

714+
export async function fetchGitHubRepositorySize(params: {
715+
token: string;
716+
owner: string;
717+
repo: string;
718+
}): Promise<string | null> {
719+
const { token, owner, repo } = params;
720+
const octokit = new Octokit({ auth: token });
721+
const { data } = await octokit.repos.get({ owner, repo });
722+
723+
if (typeof data.size !== 'number') {
724+
return null;
725+
}
726+
727+
return `${Math.round(data.size / 1024)} MiB`;
728+
}
729+
714730
export async function createGitHubBranch(params: {
715731
token: string;
716732
owner: string;

apps/web/src/lib/integrations/platforms/github/fetch-root-text-file-at-ref.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,27 @@ process.env.GITHUB_LITE_APP_ID = 'test-lite-app-id';
1111
process.env.GITHUB_LITE_APP_PRIVATE_KEY = 'test-lite-private-key';
1212

1313
const mockGetContent = jest.fn();
14+
const mockGet = jest.fn();
1415

1516
jest.mock('@octokit/rest', () => ({
1617
Octokit: jest.fn().mockImplementation(() => ({
17-
repos: { getContent: mockGetContent },
18+
repos: { getContent: mockGetContent, get: mockGet },
1819
})),
1920
}));
2021

21-
import { decodeGitHubBase64Content, fetchGitHubRootTextFileAtRef } from './adapter';
22+
import {
23+
decodeGitHubBase64Content,
24+
fetchGitHubRepositorySize,
25+
fetchGitHubRootTextFileAtRef,
26+
} from './adapter';
2227

2328
function httpError(status: number) {
2429
return Object.assign(new Error(`HTTP ${status}`), { status });
2530
}
2631

2732
beforeEach(() => {
2833
mockGetContent.mockReset();
34+
mockGet.mockReset();
2935
});
3036

3137
describe('decodeGitHubBase64Content', () => {
@@ -98,3 +104,24 @@ describe('fetchGitHubRootTextFileAtRef', () => {
98104
await expect(fetchGitHubRootTextFileAtRef(params)).rejects.toBe(error);
99105
});
100106
});
107+
108+
describe('fetchGitHubRepositorySize', () => {
109+
const params = {
110+
token: 'mock-token',
111+
owner: 'acme',
112+
repo: 'widgets',
113+
};
114+
115+
it('fetches and formats the repository size reported in KiB', async () => {
116+
mockGet.mockResolvedValueOnce({ data: { size: 102_400 } });
117+
118+
await expect(fetchGitHubRepositorySize(params)).resolves.toBe('100 MiB');
119+
expect(mockGet).toHaveBeenCalledWith({ owner: 'acme', repo: 'widgets' });
120+
});
121+
122+
it('formats zero-sized repositories explicitly', async () => {
123+
mockGet.mockResolvedValueOnce({ data: { size: 0 } });
124+
125+
await expect(fetchGitHubRepositorySize(params)).resolves.toBe('0 MiB');
126+
});
127+
});

apps/web/src/lib/integrations/platforms/gitlab/adapter.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
searchGitLabProjects,
2121
normalizeGitLabSearchQuery,
2222
fetchGitLabRootTextFileAtRef,
23+
fetchGitLabRepositorySize,
2324
} from './adapter';
2425

2526
// Mock fetch globally
@@ -770,6 +771,40 @@ describe('fetchGitLabRootTextFileAtRef', () => {
770771
});
771772
});
772773

774+
describe('fetchGitLabRepositorySize', () => {
775+
beforeEach(() => {
776+
mockFetch.mockReset();
777+
});
778+
779+
it('fetches project statistics and formats repository_size bytes as MiB', async () => {
780+
mockFetch.mockResolvedValueOnce({
781+
ok: true,
782+
json: async () => ({ statistics: { repository_size: 104_857_600 } }),
783+
});
784+
785+
const result = await fetchGitLabRepositorySize('test-token', 'group/project');
786+
787+
expect(result).toBe('100 MiB');
788+
expect(mockFetch).toHaveBeenCalledWith(
789+
'https://gitlab.com/api/v4/projects/group%2Fproject?statistics=true',
790+
expect.objectContaining({
791+
headers: {
792+
Authorization: 'Bearer test-token',
793+
},
794+
})
795+
);
796+
});
797+
798+
it('formats zero-sized repositories explicitly', async () => {
799+
mockFetch.mockResolvedValueOnce({
800+
ok: true,
801+
json: async () => ({ statistics: { repository_size: 0 } }),
802+
});
803+
804+
await expect(fetchGitLabRepositorySize('test-token', 'group/project')).resolves.toBe('0 MiB');
805+
});
806+
});
807+
773808
describe('deleteProjectWebhook', () => {
774809
beforeEach(() => {
775810
mockFetch.mockReset();

0 commit comments

Comments
 (0)