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
Original file line number Diff line number Diff line change
Expand Up @@ -144,23 +144,15 @@ export async function POST(request: NextRequest) {
: null,
]);

const commentSections: Record<string, string> = {};
const trailer = `<hr>\n\nCheck out the [code infra dashboard](${DASHBOARD_ORIGIN}/repository/${prRepo}/prs/${pr.number}) for more information about this PR.`;

if (bundleSizeReport) {
commentSections.bundleSize = bundleSizeReport.content;
}

if (benchmarkReportResult) {
commentSections.benchmark = benchmarkReportResult.content;
}

if (deployPreviewReport) {
commentSections.deployPreview = deployPreviewReport.content;
}
const commentBody = [deployPreviewReport, bundleSizeReport, benchmarkReportResult]
.filter((report): report is ReportResult => report !== null)
.map((report) => report.content)
.concat(trailer)
.join('\n\n');

await upsertPrComment(prRepo, pr.number, commentSections, {
footer: `<hr>\n\nCheck out the [code infra dashboard](${DASHBOARD_ORIGIN}/repository/${prRepo}/prs/${pr.number}) for more information about this PR.`,
});
await upsertPrComment(prRepo, pr.number, commentBody);

return NextResponse.json({ success: true });
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,12 @@ export function buildBenchmarkMarkdownReport(
lines.push(`| ${entry.name} | ${duration} | ${renders} |`);
}

const detailsLink = reportUrl ? `[details](${reportUrl})` : '';
lines.push('');
if (remaining > 0) {
const moreText = `...and ${remaining} more`;
if (reportUrl) {
lines.push('');
lines.push(`*${moreText}. [View full report](${reportUrl})*`);
} else {
lines.push('');
lines.push(`*${moreText}.*`);
}
lines.push(`*…and ${remaining} more${detailsLink ? ` — ${detailsLink}` : ''}*`);
} else if (detailsLink) {
lines.push(detailsLink);
}

return lines.join('\n');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,5 @@ export async function generateBenchmarkReport(
reportUrl: detailsUrl.toString(),
});

markdownContent += `\n\n[Details of benchmark changes](${detailsUrl})`;

return { content: `## ${BENCHMARK_SECTION_TITLE}\n\n${markdownContent}` };
}
12 changes: 6 additions & 6 deletions apps/code-infra-dashboard/src/lib/ciReports/prComment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ describe('upsertPrComment', () => {
return {};
});

const first = upsertPrComment('mui/material-ui', 42, { bundleSize: 'report 1' });
const second = upsertPrComment('mui/material-ui', 42, { bundleSize: 'report 2' });
const first = upsertPrComment('mui/material-ui', 42, 'report 1');
const second = upsertPrComment('mui/material-ui', 42, 'report 2');

resolveFirst();
await first;
Expand All @@ -73,8 +73,8 @@ describe('upsertPrComment', () => {
return {};
});

const first = upsertPrComment('mui/material-ui', 1, { bundleSize: 'report 1' });
const second = upsertPrComment('mui/material-ui', 2, { bundleSize: 'report 2' });
const first = upsertPrComment('mui/material-ui', 1, 'report 1');
const second = upsertPrComment('mui/material-ui', 2, 'report 2');

resolveFirst();
await first;
Expand All @@ -88,8 +88,8 @@ describe('upsertPrComment', () => {
.mockRejectedValueOnce(new Error('GitHub API error'))
.mockResolvedValueOnce({});

const first = upsertPrComment('mui/material-ui', 42, { bundleSize: 'report 1' });
const second = upsertPrComment('mui/material-ui', 42, { bundleSize: 'report 2' });
const first = upsertPrComment('mui/material-ui', 42, 'report 1');
const second = upsertPrComment('mui/material-ui', 42, 'report 2');

await expect(first).rejects.toThrow('GitHub API error');
await expect(second).resolves.toBeUndefined();
Expand Down
91 changes: 7 additions & 84 deletions apps/code-infra-dashboard/src/lib/ciReports/prComment.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,6 @@
import { getOctokit } from '@/lib/github';

const COMMENT_MARKER = '<!-- ci-report-comment -->';
const SECTION_REGEX = /<!-- section:(\w+) -->\n([\s\S]*?)<!-- \/section:\1 -->/g;
const HEADER_REGEX = /<!-- header -->\n([\s\S]*?)<!-- \/header -->/;
const FOOTER_REGEX = /<!-- footer -->\n([\s\S]*?)<!-- \/footer -->/;

function parseSections(body: string): Record<string, string> {
const sections: Record<string, string> = {};
for (const match of body.matchAll(SECTION_REGEX)) {
sections[match[1]] = match[2].trim();
}
return sections;
}

function parseHeader(body: string): string | null {
const match = HEADER_REGEX.exec(body);
return match ? match[1].trim() : null;
}

function parseFooter(body: string): string | null {
const match = FOOTER_REGEX.exec(body);
return match ? match[1].trim() : null;
}

function renderComment(
header: string | null,
sections: Record<string, string>,
footer: string | null,
): string {
const parts: string[] = [COMMENT_MARKER];

if (header) {
parts.push(`<!-- header -->\n${header}\n<!-- /header -->`);
}

const sectionEntries = Object.entries(sections);
if (sectionEntries.length > 0) {
parts.push(
sectionEntries
.map(([id, content]) => `<!-- section:${id} -->\n${content}\n<!-- /section:${id} -->`)
.join('\n\n'),
);
}

if (footer) {
parts.push(`<!-- footer -->\n${footer}\n<!-- /footer -->`);
}

return parts.join('\n\n');
}

/**
* Recursively searches for a comment containing the comment marker.
Expand Down Expand Up @@ -76,32 +28,19 @@ async function findComment(owner: string, repoName: string, prNumber: number, pa
return findComment(owner, repoName, prNumber, page + 1);
}

export interface UpsertPrCommentOptions {
header?: string;
footer?: string;
defaultSections?: Record<string, string>;
}

const pendingUpdates = new Map<string, Promise<void>>();

/**
* Creates or updates a comment on a pull request with section-based content.
* Each section is independently updatable — only the provided sections are
* modified, others are preserved.
* Creates or updates the CI report comment on a pull request.
*
* Concurrent calls for the same PR are serialized to prevent race conditions.
*/
export function upsertPrComment(
repo: string,
prNumber: number,
sections: Record<string, string>,
options?: UpsertPrCommentOptions,
): Promise<void> {
export function upsertPrComment(repo: string, prNumber: number, body: string): Promise<void> {
const key = `${repo}/${prNumber}`;
const prev = pendingUpdates.get(key) ?? Promise.resolve();
const next = prev
.catch(() => {})
.then(() => doUpsert(repo, prNumber, sections, options))
.then(() => doUpsert(repo, prNumber, body))
.finally(() => {
if (pendingUpdates.get(key) === next) {
pendingUpdates.delete(key);
Expand All @@ -111,12 +50,7 @@ export function upsertPrComment(
return next;
}

async function doUpsert(
repo: string,
prNumber: number,
sections: Record<string, string>,
options?: UpsertPrCommentOptions,
): Promise<void> {
async function doUpsert(repo: string, prNumber: number, body: string): Promise<void> {
const [owner, repoName] = repo.split('/');

if (!owner || !repoName) {
Expand All @@ -125,32 +59,21 @@ async function doUpsert(

const octokit = getOctokit();
const existingComment = await findComment(owner, repoName, prNumber);
const commentBody = `${COMMENT_MARKER}\n\n${body}`;

if (existingComment) {
const existingBody = existingComment.body ?? '';
const existingSections = parseSections(existingBody);
const mergedSections = {
...options?.defaultSections,
...existingSections,
...sections,
};
const mergedHeader = options?.header ?? parseHeader(existingBody);
const mergedFooter = options?.footer ?? parseFooter(existingBody);

await octokit.issues.updateComment({
owner,
repo: repoName,
comment_id: existingComment.id,
body: renderComment(mergedHeader, mergedSections, mergedFooter),
body: commentBody,
});
} else {
const mergedSections = { ...options?.defaultSections, ...sections };

await octokit.issues.createComment({
owner,
repo: repoName,
issue_number: prNumber,
body: renderComment(options?.header ?? null, mergedSections, options?.footer ?? null),
body: commentBody,
});
}
}
Loading