improve logging, remove act
This commit is contained in:
@@ -12,7 +12,6 @@ pnpm install
|
||||
|
||||
# Test with default prompt
|
||||
npm run play # Run locally on your machine
|
||||
npm run play -- --act # Run in Docker (simulates GitHub Actions)
|
||||
```
|
||||
|
||||
## Testing with play.ts
|
||||
|
||||
+32
-31
@@ -2,7 +2,7 @@ import * as core from "@actions/core";
|
||||
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { createMcpConfig } from "../mcp/config.ts";
|
||||
import { debugLog, isDebug } from "../utils/logging.ts";
|
||||
import { boxString, tableString } from "../utils/table.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { instructions } from "./shared.ts";
|
||||
import type { Agent, AgentConfig, AgentResult } from "./types.ts";
|
||||
|
||||
@@ -29,9 +29,9 @@ export class ClaudeAgent implements Agent {
|
||||
* Execute Claude Code with the given prompt using the SDK
|
||||
*/
|
||||
async execute(prompt: string): Promise<AgentResult> {
|
||||
core.info("Running Claude Agent SDK...");
|
||||
log.info("Running Claude Agent SDK...");
|
||||
|
||||
console.log(boxString(prompt, { title: "Prompt" }));
|
||||
log.box(prompt, { title: "Prompt" });
|
||||
|
||||
const mcpConfig = JSON.parse(createMcpConfig(this.githubInstallationToken));
|
||||
|
||||
@@ -59,10 +59,10 @@ export class ClaudeAgent implements Agent {
|
||||
// Stream the results
|
||||
for await (const message of queryInstance) {
|
||||
const handler = messageHandlers[message.type];
|
||||
handler(message as never);
|
||||
await handler(message as never);
|
||||
}
|
||||
|
||||
core.info("✅ Task complete.");
|
||||
log.success("Task complete.");
|
||||
core.endGroup();
|
||||
|
||||
return {
|
||||
@@ -76,7 +76,7 @@ type SDKMessageType = SDKMessage["type"];
|
||||
|
||||
type SDKMessageHandler<type extends SDKMessageType = SDKMessageType> = (
|
||||
data: Extract<SDKMessage, { type: type }>
|
||||
) => void;
|
||||
) => void | Promise<void>;
|
||||
|
||||
type SDKMessageHandlers = {
|
||||
[type in SDKMessageType]: SDKMessageHandler<type>;
|
||||
@@ -87,33 +87,33 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
if (data.message?.content) {
|
||||
for (const content of data.message.content) {
|
||||
if (content.type === "text" && content.text?.trim()) {
|
||||
core.info(boxString(content.text.trim(), { title: "Claude" }));
|
||||
log.box(content.text.trim(), { title: "Claude" });
|
||||
} else if (content.type === "tool_use") {
|
||||
core.info(`→ ${content.name}`);
|
||||
log.info(`→ ${content.name}`);
|
||||
|
||||
if (content.input) {
|
||||
const input = content.input as any;
|
||||
if (input.description) core.info(` └─ ${input.description}`);
|
||||
if (input.command) core.info(` └─ command: ${input.command}`);
|
||||
if (input.file_path) core.info(` └─ file: ${input.file_path}`);
|
||||
if (input.description) log.info(` └─ ${input.description}`);
|
||||
if (input.command) log.info(` └─ command: ${input.command}`);
|
||||
if (input.file_path) log.info(` └─ file: ${input.file_path}`);
|
||||
if (input.content) {
|
||||
const preview =
|
||||
input.content.length > 100
|
||||
? `${input.content.substring(0, 100)}...`
|
||||
: input.content;
|
||||
core.info(` └─ content: ${preview}`);
|
||||
log.info(` └─ content: ${preview}`);
|
||||
}
|
||||
if (input.query) core.info(` └─ query: ${input.query}`);
|
||||
if (input.pattern) core.info(` └─ pattern: ${input.pattern}`);
|
||||
if (input.url) core.info(` └─ url: ${input.url}`);
|
||||
if (input.query) log.info(` └─ query: ${input.query}`);
|
||||
if (input.pattern) log.info(` └─ pattern: ${input.pattern}`);
|
||||
if (input.url) log.info(` └─ url: ${input.url}`);
|
||||
if (input.edits && Array.isArray(input.edits)) {
|
||||
core.info(` └─ edits: ${input.edits.length} changes`);
|
||||
log.info(` └─ edits: ${input.edits.length} changes`);
|
||||
input.edits.forEach((edit: any, index: number) => {
|
||||
if (edit.file_path) core.info(` ${index + 1}. ${edit.file_path}`);
|
||||
if (edit.file_path) log.info(` ${index + 1}. ${edit.file_path}`);
|
||||
});
|
||||
}
|
||||
if (input.task) core.info(` └─ task: ${input.task}`);
|
||||
if (input.bash_command) core.info(` └─ bash_command: ${input.bash_command}`);
|
||||
if (input.task) log.info(` └─ task: ${input.task}`);
|
||||
if (input.bash_command) log.info(` └─ bash_command: ${input.bash_command}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,26 +123,27 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
if (data.message?.content) {
|
||||
for (const content of data.message.content) {
|
||||
if (content.type === "tool_result" && content.is_error) {
|
||||
core.warning(`❌ Tool error: ${content.content}`);
|
||||
log.warning(`Tool error: ${content.content}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
result: (data) => {
|
||||
result: async (data) => {
|
||||
if (data.subtype === "success") {
|
||||
core.info(
|
||||
tableString([
|
||||
["Cost", `$${data.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
||||
["Input Tokens", data.usage?.input_tokens || 0],
|
||||
["Output Tokens", data.usage?.output_tokens || 0],
|
||||
])
|
||||
);
|
||||
await log.table([
|
||||
[{ data: "Cost", header: true }, { data: "Input Tokens", header: true }, { data: "Output Tokens", header: true }],
|
||||
[
|
||||
`$${data.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
||||
String(data.usage?.input_tokens || 0),
|
||||
String(data.usage?.output_tokens || 0),
|
||||
],
|
||||
]);
|
||||
} else if (data.subtype === "error_max_turns") {
|
||||
core.error(`❌ Max turns reached: ${JSON.stringify(data)}`);
|
||||
log.error(`Max turns reached: ${JSON.stringify(data)}`);
|
||||
} else if (data.subtype === "error_during_execution") {
|
||||
core.error(`❌ Execution error: ${JSON.stringify(data)}`);
|
||||
log.error(`Execution error: ${JSON.stringify(data)}`);
|
||||
} else {
|
||||
core.error(`❌ Failed: ${JSON.stringify(data)}`);
|
||||
log.error(`Failed: ${JSON.stringify(data)}`);
|
||||
}
|
||||
},
|
||||
system: () => {},
|
||||
|
||||
+5
-2
@@ -5,6 +5,9 @@ export const instructions = `- use the ${mcpServerName} MCP server to interact w
|
||||
- do not under any circumstances use the gh cli
|
||||
- if prompted by a comment to respond to create a new issue, pr or anything else, after succeeding,
|
||||
also respond to the original comment with a very brief message containing a link to it
|
||||
- if prompted for to review a pr, use the diff_hint returned by mcp__${mcpServerName}__get_pull_request to get the diff
|
||||
and analyze the diff to determine the review type and body
|
||||
- if prompted to review a PR:
|
||||
(1) get PR info with mcp__${mcpServerName}__get_pull_request
|
||||
(2) fetch both branches: git fetch origin <base> --depth=20 && git fetch origin <head>
|
||||
(3) view diff: git diff origin/<base>...origin/<head>
|
||||
replace <base> and <head> with 'base' and 'head' from the PR info
|
||||
`;
|
||||
|
||||
@@ -8,10 +8,11 @@ import * as core from "@actions/core";
|
||||
import { type } from "arktype";
|
||||
import { Inputs, main } from "./main.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { log } from "./utils/cli.ts";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
|
||||
const inputsJson = process.env.INPUTS_JSON;
|
||||
if (!inputsJson) {
|
||||
|
||||
+5
-25
@@ -7,7 +7,7 @@ export const PullRequestInfo = type({
|
||||
|
||||
export const PullRequestInfoTool = tool({
|
||||
name: "get_pull_request",
|
||||
description: "Retrieve detailed information for a specific pull request by number, with a suggested local diff command.",
|
||||
description: "Retrieve minimal information for a specific pull request by number.",
|
||||
parameters: PullRequestInfo,
|
||||
execute: contextualize(async ({ pull_number }, ctx) => {
|
||||
const pr = await ctx.octokit.rest.pulls.get({
|
||||
@@ -17,16 +17,9 @@ export const PullRequestInfoTool = tool({
|
||||
});
|
||||
|
||||
const data = pr.data;
|
||||
|
||||
const baseBranch = data.base?.ref;
|
||||
const headBranch = data.head?.ref;
|
||||
const headSha = data.head?.sha;
|
||||
|
||||
// Suggest a local diff command similar to claude-code-action
|
||||
// Consumers can run this in a repo checkout to view the diff without API diff access
|
||||
const diff_hint = baseBranch
|
||||
? `git fetch origin ${baseBranch} --depth=20 && git diff origin/${baseBranch}...${headSha || "HEAD"}`
|
||||
: undefined;
|
||||
|
||||
const baseBranch = data.base.ref;
|
||||
const headBranch = data.head.ref;
|
||||
|
||||
return {
|
||||
number: data.number,
|
||||
@@ -35,21 +28,8 @@ export const PullRequestInfoTool = tool({
|
||||
state: data.state,
|
||||
draft: data.draft,
|
||||
merged: data.merged,
|
||||
mergeable: data.mergeable,
|
||||
user: data.user?.login,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
head: headBranch,
|
||||
head_sha: headSha,
|
||||
base: baseBranch,
|
||||
base_sha: data.base?.sha,
|
||||
additions: data.additions,
|
||||
deletions: data.deletions,
|
||||
changed_files: data.changed_files,
|
||||
labels: (data.labels || []).map((l) => (typeof l === "string" ? l : l.name)).filter(Boolean),
|
||||
requested_reviewers: (data.requested_reviewers || []).map((u) => u.login),
|
||||
requested_teams: (data.requested_teams || []).map((t) => t.slug),
|
||||
diff_hint,
|
||||
head: headBranch,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -6,22 +6,16 @@ import arg from "arg";
|
||||
import { config } from "dotenv";
|
||||
import { type Inputs, main } from "./main.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { runAct } from "./utils/act.ts";
|
||||
import { log } from "./utils/cli.ts";
|
||||
import { setupTestRepo } from "./utils/setup.ts";
|
||||
|
||||
config();
|
||||
|
||||
export async function run(
|
||||
prompt: string,
|
||||
options: { act?: boolean } = {}
|
||||
prompt: string
|
||||
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||
try {
|
||||
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
if (options.act) {
|
||||
console.log("🐳 Running with Docker/act...");
|
||||
runAct(prompt);
|
||||
return { success: true };
|
||||
}
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
|
||||
const tempDir = join(process.cwd(), ".temp");
|
||||
setupTestRepo({ tempDir, forceClean: true });
|
||||
@@ -29,11 +23,10 @@ export async function run(
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(tempDir);
|
||||
|
||||
console.log("🚀 Running action with prompt...");
|
||||
console.log("─".repeat(50));
|
||||
console.log("Prompt:");
|
||||
console.log(prompt);
|
||||
console.log("─".repeat(50));
|
||||
log.info("🚀 Running action with prompt...");
|
||||
log.separator();
|
||||
log.box(prompt, { title: "Prompt" });
|
||||
log.separator();
|
||||
|
||||
const inputs: Inputs = {
|
||||
prompt,
|
||||
@@ -45,15 +38,15 @@ export async function run(
|
||||
process.chdir(originalCwd);
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ Action completed successfully");
|
||||
log.success("Action completed successfully");
|
||||
return { success: true, output: result.output || undefined, error: undefined };
|
||||
} else {
|
||||
console.error("❌ Action failed:", result.error);
|
||||
log.error(`Action failed: ${result.error || "Unknown error"}`);
|
||||
return { success: false, error: result.error || undefined, output: undefined };
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
console.error("❌ Error:", errorMessage);
|
||||
} catch (err) {
|
||||
const errorMessage = (err as Error).message;
|
||||
log.error(`Error: ${errorMessage}`);
|
||||
return { success: false, error: errorMessage, output: undefined };
|
||||
}
|
||||
}
|
||||
@@ -61,7 +54,6 @@ export async function run(
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const args = arg({
|
||||
"--help": Boolean,
|
||||
"--act": Boolean,
|
||||
"--raw": String,
|
||||
"-h": "--help",
|
||||
});
|
||||
@@ -76,7 +68,6 @@ Arguments:
|
||||
file Prompt file to use (.txt, .json, or .ts) [default: fixtures/basic.txt]
|
||||
|
||||
Options:
|
||||
--act Use Docker/act to run the action instead of running directly
|
||||
--raw [prompt] Use raw string as prompt instead of loading from file
|
||||
-h, --help Show this help message
|
||||
|
||||
@@ -84,7 +75,7 @@ Examples:
|
||||
tsx play.ts # Use default fixture
|
||||
tsx play.ts fixtures/basic.txt # Use specific text file
|
||||
tsx play.ts custom.json # Use JSON file
|
||||
tsx play.ts --act fixtures/test.ts # Use TypeScript file with Docker/act
|
||||
tsx play.ts fixtures/test.ts # Use TypeScript file
|
||||
tsx play.ts --raw "Hello world" # Use raw string as prompt
|
||||
`);
|
||||
process.exit(0);
|
||||
@@ -145,13 +136,13 @@ Examples:
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await run(prompt, { act: args["--act"] || false });
|
||||
const result = await run(prompt);
|
||||
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", (error as Error).message);
|
||||
} catch (err) {
|
||||
log.error((err as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { config } from "dotenv";
|
||||
import { setupTestRepo } from "./setup.ts";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const tempDir = join(__dirname, "..", ".temp");
|
||||
const actionPath = join(__dirname, "..");
|
||||
const envPath = join(__dirname, "..", "..", ".env");
|
||||
|
||||
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
||||
|
||||
export function runAct(prompt: string): void {
|
||||
setupTestRepo({ tempDir });
|
||||
|
||||
config({ path: envPath });
|
||||
|
||||
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||
|
||||
const distPath = join(actionPath, ".act-dist");
|
||||
console.log("📦 Creating minimal distribution for act...");
|
||||
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
|
||||
|
||||
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
|
||||
const src = join(actionPath, file);
|
||||
if (existsSync(src)) {
|
||||
execSync(`cp "${src}" "${distPath}"`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||
|
||||
const actCommandParts = [
|
||||
"act",
|
||||
"workflow_dispatch",
|
||||
"-W",
|
||||
workflowPath,
|
||||
"--input",
|
||||
`prompt='${escapedPrompt}'`,
|
||||
"--local-repository",
|
||||
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
|
||||
];
|
||||
|
||||
ENV_VARS.forEach((key) => {
|
||||
if (process.env[key]) {
|
||||
actCommandParts.push("-s", key);
|
||||
}
|
||||
});
|
||||
|
||||
const actCommand = actCommandParts.join(" ");
|
||||
|
||||
console.log("🚀 Running act with prompt:");
|
||||
console.log("─".repeat(50));
|
||||
console.log(prompt);
|
||||
console.log("─".repeat(50));
|
||||
console.log("");
|
||||
|
||||
execSync(actCommand, {
|
||||
stdio: "inherit",
|
||||
cwd: join(__dirname, "..", ".."),
|
||||
});
|
||||
execSync(`rm -rf "${distPath}"`);
|
||||
} catch (error) {
|
||||
execSync(`rm -rf "${distPath}"`);
|
||||
console.error("❌ Act execution failed:", (error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user