diff --git a/packages/ci/src/lib/comment.ts b/packages/ci/src/lib/comment.ts new file mode 100644 index 000000000..a38434ce4 --- /dev/null +++ b/packages/ci/src/lib/comment.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs/promises'; +import type { Logger, ProviderAPIClient } from './models'; + +export async function commentOnPR( + mdPath: string, + api: ProviderAPIClient, + logger: Logger, +): Promise { + const markdown = await fs.readFile(mdPath, 'utf8'); + const identifier = ``; + const body = truncateBody( + `${markdown}\n\n${identifier}\n`, + api.maxCommentChars, + logger, + ); + + const comments = await api.listComments(); + logger.debug(`Fetched ${comments.length} comments for pull request`); + + const prevComment = comments.find(comment => + comment.body.includes(identifier), + ); + logger.debug( + prevComment + ? `Found previous comment ${prevComment.id} from Code PushUp` + : 'Previous Code PushUp comment not found', + ); + + if (prevComment) { + const updatedComment = await api.updateComment(prevComment.id, body); + logger.debug(`Updated body of comment ${updatedComment.url}`); + return updatedComment.id; + } + + const createdComment = await api.createComment(body); + logger.debug(`Created new comment ${createdComment.url}`); + return createdComment.id; +} + +function truncateBody(body: string, max: number, logger: Logger): string { + const truncateWarning = '...*[Comment body truncated]*'; + if (body.length > max) { + logger.warn(`Comment body is too long. Truncating to ${max} characters.`); + return body.slice(0, max - truncateWarning.length) + truncateWarning; + } + return body; +} diff --git a/packages/ci/src/lib/models.ts b/packages/ci/src/lib/models.ts index 8e7584a8e..59f4ba4c8 100644 --- a/packages/ci/src/lib/models.ts +++ b/packages/ci/src/lib/models.ts @@ -21,10 +21,17 @@ export type GitRefs = { }; export type ProviderAPIClient = { + maxCommentChars: number; downloadReportArtifact: () => Promise; - fetchComments: () => Promise<{ id: number; body: string }[]>; - updateComment: (id: number, body: string) => Promise; - createComment: (body: string) => Promise; + listComments: () => Promise; + updateComment: (id: number, body: string) => Promise; + createComment: (body: string) => Promise; +}; + +export type Comment = { + id: number; + body: string; + url: string; }; export type GitBranch = { diff --git a/packages/ci/src/lib/monorepo/list-projects.ts b/packages/ci/src/lib/monorepo/list-projects.ts index 392036ddc..6b95a5500 100644 --- a/packages/ci/src/lib/monorepo/list-projects.ts +++ b/packages/ci/src/lib/monorepo/list-projects.ts @@ -1,12 +1,11 @@ import { glob } from 'glob'; import { join } from 'node:path'; -import type { Settings } from '../models'; +import type { Logger, Settings } from '../models'; import { detectMonorepoTool } from './detect-tool'; import { getToolHandler } from './handlers'; import { listPackages } from './packages'; import type { MonorepoHandlerOptions, ProjectConfig } from './tools'; -// eslint-disable-next-line max-lines-per-function export async function listMonorepoProjects( settings: Settings, ): Promise { @@ -41,33 +40,19 @@ export async function listMonorepoProjects( } if (settings.projects) { - const directories = await glob( - settings.projects.map(path => path.replace(/\/$/, '/')), - { cwd: options.cwd }, - ); - logger.info( - `Found ${ - directories.length - } project folders matching "${settings.projects.join( - ', ', - )}" from configuration`, - ); - logger.debug(`Projects: ${directories.join(', ')}`); - return directories.toSorted().map(directory => ({ - name: directory, + return listProjectsByGlobs({ + patterns: settings.projects, + cwd: options.cwd, bin: settings.bin, - directory: join(options.cwd, directory), - })); + logger, + }); } - const packages = await listPackages(options.cwd); - logger.info(`Found ${packages.length} NPM packages in repository`); - logger.debug(`Projects: ${packages.map(({ name }) => name).join(', ')}`); - return packages.map(({ name, directory }) => ({ - name, + return listProjectsByNpmPackages({ + cwd: options.cwd, bin: settings.bin, - directory, - })); + logger, + }); } function createMonorepoHandlerOptions( @@ -88,3 +73,49 @@ function createMonorepoHandlerOptions( }), }; } + +async function listProjectsByGlobs(args: { + patterns: string[]; + cwd: string; + bin: string; + logger: Logger; +}): Promise { + const { patterns, cwd, bin, logger } = args; + + const directories = await glob( + patterns.map(path => path.replace(/\/$/, '/')), + { cwd }, + ); + + logger.info( + `Found ${directories.length} project folders matching "${patterns.join( + ', ', + )}" from configuration`, + ); + logger.debug(`Projects: ${directories.join(', ')}`); + + return directories.toSorted().map(directory => ({ + name: directory, + bin, + directory: join(cwd, directory), + })); +} + +async function listProjectsByNpmPackages(args: { + cwd: string; + bin: string; + logger: Logger; +}): Promise { + const { cwd, bin, logger } = args; + + const packages = await listPackages(cwd); + + logger.info(`Found ${packages.length} NPM packages in repository`); + logger.debug(`Projects: ${packages.map(({ name }) => name).join(', ')}`); + + return packages.map(({ name, directory }) => ({ + name, + bin, + directory, + })); +}