316 lines
7.1 KiB
TypeScript
316 lines
7.1 KiB
TypeScript
/**
|
|
* CLI output utilities for GitHub Actions and local development
|
|
* Uses GitHub Actions Job Summaries API when available for rich formatting
|
|
*/
|
|
|
|
import * as core from "@actions/core";
|
|
|
|
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
|
|
/**
|
|
* Print a formatted box with text (for console output)
|
|
*/
|
|
function boxString(
|
|
text: string,
|
|
options?: {
|
|
title?: string;
|
|
maxWidth?: number;
|
|
indent?: string;
|
|
padding?: number;
|
|
}
|
|
): string {
|
|
const { title, maxWidth = 80, indent = "", padding = 1 } = options || {};
|
|
|
|
const lines = text.trim().split("\n");
|
|
const wrappedLines: string[] = [];
|
|
|
|
for (const line of lines) {
|
|
if (line.length <= maxWidth - padding * 2) {
|
|
wrappedLines.push(line);
|
|
} else {
|
|
const words = line.split(" ");
|
|
let currentLine = "";
|
|
|
|
for (const word of words) {
|
|
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
if (testLine.length <= maxWidth - padding * 2) {
|
|
currentLine = testLine;
|
|
} else {
|
|
if (currentLine) {
|
|
wrappedLines.push(currentLine);
|
|
currentLine = word;
|
|
} else {
|
|
wrappedLines.push(word.substring(0, maxWidth - padding * 2));
|
|
currentLine = word.substring(maxWidth - padding * 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentLine) {
|
|
wrappedLines.push(currentLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
const maxLineLength = Math.max(...wrappedLines.map((line) => line.length));
|
|
const boxWidth = maxLineLength + padding * 2;
|
|
|
|
let result = "";
|
|
|
|
if (title) {
|
|
const titleLine = ` ${title} `;
|
|
const titlePadding = Math.max(0, boxWidth - titleLine.length);
|
|
result += `${indent}┌${titleLine}${"─".repeat(titlePadding)}┐\n`;
|
|
}
|
|
|
|
if (!title) {
|
|
result += `${indent}┌${"─".repeat(boxWidth)}┐\n`;
|
|
}
|
|
|
|
for (const line of wrappedLines) {
|
|
const paddedLine = line.padEnd(maxLineLength);
|
|
result += `${indent}│${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}│\n`;
|
|
}
|
|
|
|
result += `${indent}└${"─".repeat(boxWidth)}┘`;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Print a formatted box with text
|
|
* In GitHub Actions, uses markdown code blocks; locally uses box formatting
|
|
*/
|
|
function box(
|
|
text: string,
|
|
options?: {
|
|
title?: string;
|
|
maxWidth?: number;
|
|
}
|
|
): void {
|
|
if (isGitHubActions) {
|
|
const { title } = options || {};
|
|
let markdown = "";
|
|
if (title) {
|
|
markdown += `### ${title}\n\n`;
|
|
}
|
|
markdown += "```\n";
|
|
markdown += text;
|
|
markdown += "\n```\n";
|
|
// Note: summary.write() should be called once at the end, but for immediate visibility we call it here
|
|
// In practice, you might want to batch multiple calls
|
|
core.summary.addRaw(markdown);
|
|
core.info(markdown);
|
|
} else {
|
|
log.info(boxString(text, options));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a table using GitHub Actions Job Summaries API
|
|
* Falls back to formatted console table locally
|
|
*/
|
|
async function table(
|
|
rows: Array<Array<{ data: string; header?: boolean } | string>>,
|
|
options?: {
|
|
title?: string;
|
|
}
|
|
): Promise<void> {
|
|
const { title } = options || {};
|
|
|
|
if (isGitHubActions) {
|
|
const summary = core.summary;
|
|
|
|
if (title) {
|
|
summary.addHeading(title);
|
|
}
|
|
|
|
// Convert rows to format expected by Job Summaries API
|
|
const formattedRows = rows.map((row) =>
|
|
row.map((cell) => {
|
|
if (typeof cell === "string") {
|
|
return { data: cell };
|
|
}
|
|
return cell;
|
|
})
|
|
);
|
|
|
|
summary.addTable(formattedRows);
|
|
await summary.write();
|
|
|
|
// Also log to console for visibility in logs
|
|
const tableText = formattedRows
|
|
.map((row) => row.map((cell) => cell.data).join(" | "))
|
|
.join("\n");
|
|
log.info(tableText);
|
|
} else {
|
|
// Local fallback: simple console table
|
|
if (title) {
|
|
log.info(`\n${title}`);
|
|
}
|
|
const tableText = rows
|
|
.map((row) => {
|
|
const rowData = row.map((cell) => {
|
|
const text = typeof cell === "string" ? cell : cell.data;
|
|
const isHeader = typeof cell !== "string" && cell.header;
|
|
return isHeader ? `**${text}**` : text;
|
|
});
|
|
return rowData.join(" | ");
|
|
})
|
|
.join("\n");
|
|
log.info(`\n${tableText}\n`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add raw markdown to job summary (GitHub Actions) or print as info (local)
|
|
*/
|
|
async function markdown(content: string): Promise<void> {
|
|
if (isGitHubActions) {
|
|
core.summary.addRaw(content);
|
|
await core.summary.write();
|
|
// Also log to console
|
|
core.info(content);
|
|
} else {
|
|
log.info(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a code block to output
|
|
*/
|
|
function codeBlock(code: string, language?: string): void {
|
|
if (isGitHubActions) {
|
|
core.summary.addCodeBlock(code, language);
|
|
core.info(`\`\`\`${language || ""}\n${code}\n\`\`\``);
|
|
} else {
|
|
log.info(`\`\`\`${language || ""}\n${code}\n\`\`\``);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a link to output
|
|
*/
|
|
function link(text: string, url: string): void {
|
|
if (isGitHubActions) {
|
|
core.summary.addLink(text, url);
|
|
log.info(`[${text}](${url})`);
|
|
} else {
|
|
log.info(`[${text}](${url})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write all pending summary changes to the job summary
|
|
* Call this at the end of your action to ensure all summary content is written
|
|
*/
|
|
async function writeSummary(): Promise<void> {
|
|
if (isGitHubActions) {
|
|
await core.summary.write();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print a separator line
|
|
*/
|
|
function separator(length: number = 50): void {
|
|
if (isGitHubActions) {
|
|
core.info("─".repeat(length));
|
|
} else {
|
|
console.log("─".repeat(length));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main logging utility object - import this once and access all utilities
|
|
*/
|
|
export const log = {
|
|
/**
|
|
* Check if running in GitHub Actions environment
|
|
*/
|
|
isGitHubActions: (): boolean => isGitHubActions,
|
|
|
|
/**
|
|
* Print info message (GitHub Actions or console)
|
|
*/
|
|
info: (message: string): void => {
|
|
if (isGitHubActions) {
|
|
core.info(message);
|
|
} else {
|
|
console.log(message);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Print warning message
|
|
*/
|
|
warning: (message: string): void => {
|
|
if (isGitHubActions) {
|
|
core.warning(message);
|
|
} else {
|
|
console.warn(`⚠️ ${message}`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Print error message
|
|
*/
|
|
error: (message: string): void => {
|
|
if (isGitHubActions) {
|
|
core.error(message);
|
|
} else {
|
|
console.error(`❌ ${message}`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Print success message
|
|
*/
|
|
success: (message: string): void => {
|
|
const msg = `✅ ${message}`;
|
|
if (isGitHubActions) {
|
|
core.info(msg);
|
|
} else {
|
|
console.log(msg);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Print a formatted box with text
|
|
* In GitHub Actions, uses markdown code blocks; locally uses box formatting
|
|
*/
|
|
box,
|
|
|
|
/**
|
|
* Add a table using GitHub Actions Job Summaries API
|
|
* Falls back to formatted console table locally
|
|
*/
|
|
table,
|
|
|
|
/**
|
|
* Add raw markdown to job summary (GitHub Actions) or print as info (local)
|
|
*/
|
|
markdown,
|
|
|
|
/**
|
|
* Add a code block to output
|
|
*/
|
|
codeBlock,
|
|
|
|
/**
|
|
* Add a link to output
|
|
*/
|
|
link,
|
|
|
|
/**
|
|
* Write all pending summary changes to the job summary
|
|
* Call this at the end of your action to ensure all summary content is written
|
|
*/
|
|
writeSummary,
|
|
|
|
/**
|
|
* Print a separator line
|
|
*/
|
|
separator,
|
|
};
|