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
1 change: 1 addition & 0 deletions packages/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@code-pushup/models": "0.91.0",
"@code-pushup/portal-client": "^0.16.0",
"@code-pushup/utils": "0.91.0",
"ansis": "^3.3.2",
"glob": "^11.0.1",
"simple-git": "^3.20.0",
"yaml": "^2.5.1",
Expand Down
81 changes: 51 additions & 30 deletions packages/ci/src/lib/cli/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,56 @@ import {
} from '@code-pushup/utils';
import type { CommandContext } from './context.js';

/**
* Executes Code PushUp CLI command and logs output in a way that's more readable in CI.
* @param args Arguments for Code PushUp CLI
* @param context Command context
* @param options Optional information on whether all persist formats are set (if known)
*/
export async function executeCliCommand(
args: string[],
context: CommandContext,
options?: { hasFormats: boolean },
): Promise<void> {
const { logOutputChunk, logOutputEnd, logSilencedOutput } =
createLogCallbacks(context);

const observer: ProcessObserver = {
onStdout: logOutputChunk,
onStderr: logOutputChunk,
onComplete: logOutputEnd,
onError: logOutputEnd,
};

const config: ProcessConfig = {
command: context.bin,
args: combineArgs(args, context, options),
cwd: context.directory,
observer,
silent: true,
};
const bin = serializeCommandWithArgs(config);

try {
await logger.command(bin, async () => {
try {
await executeProcess(config);
} catch (error) {
// ensure output of failed process is always logged for debugging
logSilencedOutput();
throw error;
}
});
} finally {
logger.newline();
}
}

function createLogCallbacks(context: Pick<CommandContext, 'silent'>) {
// eslint-disable-next-line functional/no-let
let output = '';

const logRaw = (message: string) => {
const logOutputChunk = (message: string) => {
if (!context.silent) {
if (!output) {
logger.newline();
Expand All @@ -26,43 +67,23 @@ export async function executeCliCommand(
output += message;
};

const logEnd = () => {
const logOutputEnd = () => {
if (!context.silent && output) {
logger.newline();
}
};

const observer: ProcessObserver = {
onStdout: logRaw,
onStderr: logRaw,
onComplete: logEnd,
onError: logEnd,
};

const config: ProcessConfig = {
command: context.bin,
args: combineArgs(args, context, options),
cwd: context.directory,
observer,
silent: true,
};
const bin = serializeCommandWithArgs(config);

await logger.command(bin, async () => {
try {
await executeProcess(config);
} catch (error) {
// ensure output of failed process is always logged for debugging
if (context.silent) {
const logSilencedOutput = () => {
if (context.silent) {
logger.newline();
logger.info(output, { noIndent: true });
if (!output.endsWith('\n')) {
logger.newline();
logger.info(output, { noIndent: true });
if (!output.endsWith('\n')) {
logger.newline();
}
}
throw error;
}
});
};

return { logOutputChunk, logOutputEnd, logSilencedOutput };
}

function combineArgs(
Expand Down
6 changes: 6 additions & 0 deletions packages/ci/src/lib/cli/exec.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ describe('executeCliCommand', () => {
`
- $ npx code-pushup
✔ $ npx code-pushup (42 ms)

`.trimStart(),
);
});
Expand All @@ -152,6 +153,7 @@ WARN: API key is missing, skipping upload
Collected report files in .code-pushup directory

✔ $ npx code-pushup (42 ms)

`.trimStart(),
);
});
Expand Down Expand Up @@ -197,6 +199,7 @@ Collected report
Uploaded report to portal

✔ $ npx code-pushup (42 ms)

`.trimStart(),
);
});
Expand All @@ -222,6 +225,7 @@ Code PushUp CLI v0.42.0
ERROR: Config file not found

✖ $ npx code-pushup

`.trimStart(),
);
});
Expand All @@ -236,6 +240,7 @@ ERROR: Config file not found
`
- $ npx code-pushup
✔ $ npx code-pushup (42 ms)

`.trimStart(),
);
});
Expand All @@ -261,6 +266,7 @@ Code PushUp CLI v0.42.0
ERROR: Config file not found

✖ $ npx code-pushup

`.trimStart(),
);
});
Expand Down
12 changes: 6 additions & 6 deletions packages/ci/src/lib/comment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises';
import { logger } from '@code-pushup/utils';
import { logDebug, logInfo, logWarning } from './log.js';
import type { ProviderAPIClient, Settings } from './models.js';

export async function commentOnPR(
Expand All @@ -17,32 +17,32 @@ export async function commentOnPR(
);

const comments = await api.listComments();
logger.debug(`Fetched ${comments.length} comments for pull request`);
logDebug(`Fetched ${comments.length} comments for pull request`);

const prevComment = comments.find(comment =>
comment.body.includes(identifier),
);
logger.debug(
logDebug(
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.info(`Updated body of comment ${updatedComment.url}`);
logInfo(`Updated body of comment ${updatedComment.url}`);
return updatedComment.id;
}

const createdComment = await api.createComment(body);
logger.info(`Created new comment ${createdComment.url}`);
logInfo(`Created new comment ${createdComment.url}`);
return createdComment.id;
}

function truncateBody(body: string, max: number): string {
const truncateWarning = '...*[Comment body truncated]*';
if (body.length > max) {
logger.warn(`Comment body is too long. Truncating to ${max} characters.`);
logWarning(`Comment body is too long. Truncating to ${max} characters.`);
return body.slice(0, max - truncateWarning.length) + truncateWarning;
}
return body;
Expand Down
3 changes: 2 additions & 1 deletion packages/ci/src/lib/comment.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ansis from 'ansis';
import { vol } from 'memfs';
import { writeFile } from 'node:fs/promises';
import path from 'node:path';
Expand Down Expand Up @@ -112,7 +113,7 @@ describe('commentOnPR', () => {
expect.stringContaining('...*[Comment body truncated]*'),
);
expect(logger.warn).toHaveBeenCalledWith(
'Comment body is too long. Truncating to 1000000 characters.',
`${ansis.bold.blue('<✓>')} Comment body is too long. Truncating to 1000000 characters.\n`,
);
});
});
59 changes: 59 additions & 0 deletions packages/ci/src/lib/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import ansis from 'ansis';
import {
CODE_PUSHUP_UNICODE_LOGO,
logger,
transformLines,
} from '@code-pushup/utils';

const LOG_PREFIX = ansis.bold.blue(CODE_PUSHUP_UNICODE_LOGO);

/**
* Logs error message with top-level CI log styles (lines prefixed with logo, ends in empty line).
* @param message Log message
*/
export function logError(message: string): void {
log('error', message);
}

/**
* Logs warning message with top-level CI log styles (lines prefixed with logo, ends in empty line).
* @param message Log message
*/
export function logWarning(message: string): void {
log('warn', message);
}

/**
* Logs info message with top-level CI log styles (lines prefixed with logo, ends in empty line).
* @param message Log message
*/
export function logInfo(message: string): void {
log('info', message);
}

/**
* Logs debug message with top-level CI log styles (lines prefixed with logo, ends in empty line).
* @param message Log message
*/
export function logDebug(message: string): void {
log('debug', message);
}

/**
* Prefixes CI logs with logo and ensures each CI log is followed by an empty line.
* This is to make top-level CI logs more visually distinct from printed process logs.
* @param level Log level
* @param message Log message
*/
export function log(
level: 'error' | 'warn' | 'info' | 'debug',
message: string,
): void {
const prefixedLines = transformLines(
message.trim(),
line => `${LOG_PREFIX} ${line}`,
);
const styledMessage = `${prefixedLines}\n`;

logger[level](styledMessage);
}
31 changes: 31 additions & 0 deletions packages/ci/src/lib/log.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ansis from 'ansis';
import { logger } from '@code-pushup/utils';
import { log } from './log.js';

describe('log', () => {

Check failure on line 5 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
it('should add logo prefix and ending line-break to message', () => {

Check failure on line 6 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
log('info', 'Running Code PushUp in standalone mode');
expect(logger.info).toHaveBeenCalledWith(

Check failure on line 8 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
`${ansis.bold.blue('<✓>')} Running Code PushUp in standalone mode\n`,
);
});

it('should add logo prefix to each line', () => {

Check failure on line 13 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
log('debug', 'Found 3 Nx projects:\n- api\n- backoffice\n- frontoffice');
expect(logger.debug).toHaveBeenCalledWith(

Check failure on line 15 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
`
${ansis.bold.blue('<✓>')} Found 3 Nx projects:
${ansis.bold.blue('<✓>')} - api
${ansis.bold.blue('<✓>')} - backoffice
${ansis.bold.blue('<✓>')} - frontoffice
`.trimStart(),
);
});

it('should not add final line-break if already present', () => {

Check failure on line 25 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
log('warn', 'Comment body is too long, truncating to 1000 characters\n');
expect(logger.warn).toHaveBeenCalledWith(

Check failure on line 27 in packages/ci/src/lib/log.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
`${ansis.bold.blue('<✓>')} Comment body is too long, truncating to 1000 characters\n`,
);
});
});
20 changes: 10 additions & 10 deletions packages/ci/src/lib/monorepo/list-projects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { glob } from 'glob';
import path from 'node:path';
import { logger } from '@code-pushup/utils';
import { logDebug, logInfo } from '../log.js';
import type { Settings } from '../models.js';
import { detectMonorepoTool } from './detect-tool.js';
import { getToolHandler } from './handlers/index.js';
Expand Down Expand Up @@ -31,8 +31,8 @@ export async function listMonorepoProjects(
if (tool) {
const handler = getToolHandler(tool);
const projects = await handler.listProjects(options);
logger.info(`Found ${projects.length} projects in ${tool} monorepo`);
logger.debug(`Projects: ${projects.map(({ name }) => name).join(', ')}`);
logInfo(`Found ${projects.length} projects in ${tool} monorepo`);
logDebug(`Projects: ${projects.map(({ name }) => name).join(', ')}`);
return {
tool,
projects,
Expand Down Expand Up @@ -70,15 +70,15 @@ async function resolveMonorepoTool(
}

if (typeof settings.monorepo === 'string') {
logger.info(`Using monorepo tool "${settings.monorepo}" from inputs`);
logInfo(`Using monorepo tool "${settings.monorepo}" from inputs`);
return settings.monorepo;
}

const tool = await detectMonorepoTool(options);
if (tool) {
logger.info(`Auto-detected monorepo tool ${tool}`);
logInfo(`Auto-detected monorepo tool ${tool}`);
} else {
logger.info("Couldn't auto-detect any supported monorepo tool");
logInfo("Couldn't auto-detect any supported monorepo tool");
}

return tool;
Expand Down Expand Up @@ -110,12 +110,12 @@ async function listProjectsByGlobs(args: {
{ cwd },
);

logger.info(
logInfo(
`Found ${directories.length} project folders matching "${patterns.join(
', ',
)}" from configuration`,
);
logger.debug(`Projects: ${directories.join(', ')}`);
logDebug(`Projects: ${directories.join(', ')}`);

return directories.toSorted().map(directory => ({
name: directory,
Expand All @@ -132,8 +132,8 @@ async function listProjectsByNpmPackages(args: {

const packages = await listPackages(cwd);

logger.info(`Found ${packages.length} NPM packages in repository`);
logger.debug(`Projects: ${packages.map(({ name }) => name).join(', ')}`);
logInfo(`Found ${packages.length} NPM packages in repository`);
logDebug(`Projects: ${packages.map(({ name }) => name).join(', ')}`);

return packages.map(({ name, directory }) => ({
name,
Expand Down
Loading