From 15a7154aeab9432d6517446da25f008a6ff5a86c Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 31 Oct 2025 01:58:43 -0400 Subject: [PATCH] improve logging --- agents/claude.ts | 55 ++++++----------------------------------------- agents/types.ts | 25 +++++++-------------- main.ts | 33 ++++++++++++++++++++-------- mcp/pr.ts | 3 ++- mcp/prInfo.ts | 2 +- mcp/review.ts | 34 +++++++++++++++++------------ mcp/server.ts | 2 +- package.json | 2 +- play.ts | 2 +- utils/cli.ts | 56 +++++++++++++++++++++++++++++++++++++++++++----- utils/github.ts | 11 +++++----- utils/logging.ts | 30 -------------------------- utils/setup.ts | 19 ++++++++-------- 13 files changed, 132 insertions(+), 142 deletions(-) delete mode 100644 utils/logging.ts diff --git a/agents/claude.ts b/agents/claude.ts index b2f7d01..77a9ada 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -1,56 +1,17 @@ -import * as core from "@actions/core"; import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk"; -import { createMcpConfig } from "../mcp/config.ts"; import { log } from "../utils/cli.ts"; -import { debugLog, isDebug } from "../utils/logging.ts"; import { instructions } from "./shared.ts"; -import type { Agent, AgentConfig, AgentResult } from "./types.ts"; - -/** - * Claude Code agent implementation - */ -export class ClaudeAgent implements Agent { - private apiKey: string; - private githubInstallationToken: string; - - constructor(config: AgentConfig) { - this.apiKey = config.apiKey; - this.githubInstallationToken = config.githubInstallationToken; - } - - /** - * Install is a no-op since Claude CLI is bundled with the SDK - */ - async install(): Promise { - // No installation needed - CLI is bundled with @anthropic-ai/claude-agent-sdk - } - - /** - * Execute Claude Code with the given prompt using the SDK - */ - async execute(prompt: string): Promise { - log.info("Running Claude Agent SDK..."); - - log.box(prompt, { title: "Prompt" }); - - const mcpConfig = JSON.parse(createMcpConfig(this.githubInstallationToken)); - - if (isDebug()) { - debugLog(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`); - } - - // Initialize session - core.info(`🚀 Starting Claude Agent SDK session...`); - - // Set API key environment variable for SDK - process.env.ANTHROPIC_API_KEY = this.apiKey; +import type { Agent } from "./types.ts"; +export const claude: Agent = { + run: async ({ prompt, mcpServers, apiKey }) => { + process.env.ANTHROPIC_API_KEY = apiKey; // Create the query with SDK options const queryInstance = query({ prompt: `${instructions}\n\n${prompt}`, options: { permissionMode: "bypassPermissions", - mcpServers: mcpConfig.mcpServers, + mcpServers, }, }); @@ -60,14 +21,12 @@ export class ClaudeAgent implements Agent { await handler(message as never); } - log.success("Task complete."); - return { success: true, output: "", }; - } -} + }, +}; type SDKMessageType = SDKMessage["type"]; diff --git a/agents/types.ts b/agents/types.ts index e963c9b..77770fe 100644 --- a/agents/types.ts +++ b/agents/types.ts @@ -1,19 +1,4 @@ -/** - * Standard interface for all Pullfrog agents - */ -export interface Agent { - /** - * Install the agent and any required dependencies - */ - install(): Promise; - - /** - * Execute the agent with the given prompt - * @param prompt The prompt to send to the agent - * @param options Additional options specific to the agent - */ - execute(prompt: string, options?: Record): Promise; -} +import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; /** * Result returned by agent execution @@ -22,7 +7,7 @@ export interface AgentResult { success: boolean; output?: string; error?: string; - metadata?: Record; + metadata?: Record; } /** @@ -31,4 +16,10 @@ export interface AgentResult { export interface AgentConfig { apiKey: string; githubInstallationToken: string; + prompt: string; + mcpServers: Record; } + +export type Agent = { + run: (config: AgentConfig) => Promise; +}; diff --git a/main.ts b/main.ts index 7bceb29..86d5c19 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,7 @@ -import * as core from "@actions/core"; import { type } from "arktype"; -import { ClaudeAgent } from "./agents/claude.ts"; +import { claude } from "./agents/claude.ts"; +import { createMcpConfig } from "./mcp/config.ts"; +import { log } from "./utils/cli.ts"; import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts"; import { setupGitAuth, setupGitConfig } from "./utils/setup.ts"; @@ -19,7 +20,7 @@ export interface MainResult { export async function main(inputs: Inputs): Promise { try { - core.info(`→ Starting agent run with Claude Code`); + log.info("Starting agent run..."); setupGitConfig(); @@ -28,15 +29,24 @@ export async function main(inputs: Inputs): Promise { setupGitAuth(githubInstallationToken, repoContext); - const agent = new ClaudeAgent({ - apiKey: inputs.anthropic_api_key!, + // Create MCP config + const mcpConfig = JSON.parse(createMcpConfig(githubInstallationToken)); + + log.debug(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`); + + // Extract mcpServers object from config + const mcpServers = mcpConfig.mcpServers || {}; + + log.info("Running Claude Agent SDK..."); + log.box(inputs.prompt, { title: "Prompt" }); + + const result = await claude.run({ + prompt: inputs.prompt, + mcpServers, githubInstallationToken, + apiKey: inputs.anthropic_api_key!, }); - await agent.install(); - - const result = await agent.execute(inputs.prompt); - if (!result.success) { return { success: false, @@ -45,12 +55,17 @@ export async function main(inputs: Inputs): Promise { }; } + log.success("Task complete."); + await log.writeSummary(); + return { success: true, output: result.output || "", }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + log.error(errorMessage); + await log.writeSummary(); return { success: false, error: errorMessage, diff --git a/mcp/pr.ts b/mcp/pr.ts index 0887926..1e14350 100644 --- a/mcp/pr.ts +++ b/mcp/pr.ts @@ -1,5 +1,6 @@ import { execSync } from "node:child_process"; import { type } from "arktype"; +import { log } from "../utils/cli.ts"; import { contextualize, tool } from "./shared.ts"; export const PullRequest = type({ @@ -18,7 +19,7 @@ export const PullRequestTool = tool({ encoding: "utf8", }).trim(); - console.log(`Current branch: ${currentBranch}`); + log.info(`Current branch: ${currentBranch}`); const result = await ctx.octokit.rest.pulls.create({ owner: ctx.owner, diff --git a/mcp/prInfo.ts b/mcp/prInfo.ts index ae82d41..8c5ee3f 100644 --- a/mcp/prInfo.ts +++ b/mcp/prInfo.ts @@ -17,7 +17,7 @@ export const PullRequestInfoTool = tool({ }); const data = pr.data; - + const baseBranch = data.base.ref; const headBranch = data.head.ref; diff --git a/mcp/review.ts b/mcp/review.ts index cd0918a..5563fc7 100644 --- a/mcp/review.ts +++ b/mcp/review.ts @@ -1,26 +1,32 @@ +import type { RestEndpointMethodTypes } from "@octokit/rest"; import { type } from "arktype"; import { contextualize, tool } from "./shared.ts"; -import type { RestEndpointMethodTypes } from "@octokit/rest"; export const Review = type({ pull_number: type.number.describe("The pull request number to review"), - event: type.enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT").describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"), - body: type.string.describe("The body content for the review (required for REQUEST_CHANGES or COMMENT)").optional(), - commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(), - comments: type -({ - path: type.string.describe("The file path to comment on"), - position: type.number.describe("The diff position in the file"), - body: type.string.describe("The comment text"), - }) - .array() - .describe("Array of draft review comments for specific lines, optional.") - .optional() + event: type + .enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT") + .describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"), + body: type.string + .describe("The body content for the review (required for REQUEST_CHANGES or COMMENT)") + .optional(), + commit_id: type.string + .describe("Optional SHA of the commit being reviewed. Defaults to latest.") + .optional(), + comments: type({ + path: type.string.describe("The file path to comment on"), + position: type.number.describe("The diff position in the file"), + body: type.string.describe("The comment text"), + }) + .array() + .describe("Array of draft review comments for specific lines, optional.") + .optional(), }); export const ReviewTool = tool({ name: "submit_pull_request_review", - description: "Submit a review (approve, request changes, or comment) for an existing pull request.", + description: + "Submit a review (approve, request changes, or comment) for an existing pull request.", parameters: Review, execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => { // Compose the request diff --git a/mcp/server.ts b/mcp/server.ts index 19c4e9d..a58eea4 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -4,8 +4,8 @@ import { FastMCP } from "fastmcp"; import { CommentTool } from "./comment.ts"; import { IssueTool } from "./issue.ts"; import { PullRequestTool } from "./pr.ts"; -import { ReviewTool } from "./review.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; +import { ReviewTool } from "./review.ts"; import { addTools } from "./shared.ts"; const server = new FastMCP({ diff --git a/package.json b/package.json index 5bf106a..9be8e71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.65", + "version": "0.0.66", "type": "module", "files": [ "index.js", diff --git a/play.ts b/play.ts index c43d9c8..3c2e77b 100644 --- a/play.ts +++ b/play.ts @@ -59,7 +59,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { }); if (args["--help"]) { - console.log(` + log.info(` Usage: tsx play.ts [file] [options] Test the Pullfrog action with various prompts. diff --git a/utils/cli.ts b/utils/cli.ts index d6ebd79..d812fe0 100644 --- a/utils/cli.ts +++ b/utils/cli.ts @@ -5,6 +5,7 @@ import * as core from "@actions/core"; const isGitHubActions = !!process.env.GITHUB_ACTIONS; +const isDebugEnabled = process.env.LOG_LEVEL === "debug"; /** * Print a formatted box with text (for console output) @@ -87,7 +88,12 @@ function box( maxWidth?: number; } ): void { - core.info(boxString(text, options)); + const boxContent = boxString(text, options); + core.info(boxContent); + if (isGitHubActions) { + // Add as markdown code block for summary (no headers) + core.summary.addRaw(`\`\`\`\n${text}\n\`\`\`\n`); + } } /** @@ -115,10 +121,10 @@ async function summaryTable( if (isGitHubActions) { const summary = core.summary; if (title) { - summary.addHeading(title); + summary.addRaw(`**${title}**\n\n`); } summary.addTable(formattedRows); - await summary.write(); + // Note: Don't write immediately, let it accumulate with other summary content } // Also log to console for visibility @@ -133,7 +139,11 @@ async function summaryTable( * Print a separator line */ function separator(length: number = 50): void { - core.info("─".repeat(length)); + const separatorText = "─".repeat(length); + core.info(separatorText); + if (isGitHubActions) { + core.summary.addRaw(`---\n`); + } } /** @@ -145,6 +155,9 @@ export const log = { */ info: (message: string): void => { core.info(message); + if (isGitHubActions) { + core.summary.addRaw(`${message}\n`); + } }, /** @@ -152,6 +165,9 @@ export const log = { */ warning: (message: string): void => { core.warning(message); + if (isGitHubActions) { + core.summary.addRaw(`⚠️ ${message}\n`); + } }, /** @@ -159,13 +175,33 @@ export const log = { */ error: (message: string): void => { core.error(message); + if (isGitHubActions) { + core.summary.addRaw(`❌ ${message}\n`); + } }, /** * Print success message */ success: (message: string): void => { - core.info(`✅ ${message}`); + const successMessage = `✅ ${message}`; + core.info(successMessage); + if (isGitHubActions) { + core.summary.addRaw(`${successMessage}\n`); + } + }, + + /** + * Print debug message (only if LOG_LEVEL=debug) + */ + debug: (message: string): void => { + if (isDebugEnabled) { + if (isGitHubActions) { + core.debug(message); + } else { + core.info(`[DEBUG] ${message}`); + } + } }, /** @@ -183,4 +219,14 @@ export const log = { * Print a separator line */ separator, + + /** + * Write all accumulated summary content to the job summary + * Call this at the end of execution to finalize the summary + */ + writeSummary: async (): Promise => { + if (isGitHubActions) { + await core.summary.write(); + } + }, }; diff --git a/utils/github.ts b/utils/github.ts index f0091bf..c300f19 100644 --- a/utils/github.ts +++ b/utils/github.ts @@ -1,5 +1,6 @@ import { createSign } from "node:crypto"; import * as core from "@actions/core"; +import { log } from "./cli.ts"; export interface InstallationToken { token: string; @@ -53,14 +54,14 @@ function isGitHubActionsEnvironment(): boolean { } async function acquireTokenViaOIDC(): Promise { - core.info("Generating OIDC token..."); + log.info("Generating OIDC token..."); const oidcToken = await core.getIDToken("pullfrog-api"); - core.info("OIDC token generated successfully"); + log.info("OIDC token generated successfully"); const apiUrl = process.env.API_URL || "https://pullfrog.ai"; - core.info("Exchanging OIDC token for installation token..."); + log.info("Exchanging OIDC token for installation token..."); const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, { method: "POST", headers: { @@ -77,7 +78,7 @@ async function acquireTokenViaOIDC(): Promise { } const tokenData = (await tokenResponse.json()) as InstallationToken; - core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`); + log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`); return tokenData.token; } @@ -238,7 +239,7 @@ export async function setupGitHubInstallationToken(): Promise { const existingToken = checkExistingToken(); if (existingToken) { core.setSecret(existingToken); - core.info("Using provided GitHub installation token"); + log.info("Using provided GitHub installation token"); return existingToken; } diff --git a/utils/logging.ts b/utils/logging.ts deleted file mode 100644 index 8c979cf..0000000 --- a/utils/logging.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Centralized debug logging utility - * Controls debug behavior based on LOG_LEVEL environment variable - */ - -import * as core from "@actions/core"; - -const isDebugEnabled = process.env.LOG_LEVEL === "debug"; -const isGitHubActions = !!process.env.GITHUB_ACTIONS; - -/** - * Check if debug logging is enabled - */ -export function isDebug(): boolean { - return isDebugEnabled; -} - -/** - * Log debug message if debug is enabled - * Uses core.debug() in GitHub Actions, console.log() locally - */ -export function debugLog(message: string): void { - if (isDebugEnabled) { - if (isGitHubActions) { - core.debug(message); - } else { - console.log(`[DEBUG] ${message}`); - } - } -} \ No newline at end of file diff --git a/utils/setup.ts b/utils/setup.ts index 7a3710b..86cc841 100644 --- a/utils/setup.ts +++ b/utils/setup.ts @@ -1,5 +1,6 @@ import { execSync } from "node:child_process"; import { existsSync, rmSync } from "node:fs"; +import { log } from "./cli.ts"; import type { RepoContext } from "./github.ts"; export interface SetupOptions { @@ -20,20 +21,20 @@ export function setupTestRepo(options: SetupOptions): void { if (existsSync(tempDir)) { if (forceClean) { - console.log("🗑️ Removing existing .temp directory..."); + log.info("🗑️ Removing existing .temp directory..."); rmSync(tempDir, { recursive: true, force: true }); - console.log("📦 Cloning pullfrogai/scratch into .temp..."); + log.info("📦 Cloning pullfrogai/scratch into .temp..."); execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" }); } else { - console.log("📦 Resetting existing .temp repository..."); + log.info("📦 Resetting existing .temp repository..."); execSync("git reset --hard HEAD && git clean -fd", { cwd: tempDir, stdio: "inherit", }); } } else { - console.log("📦 Cloning pullfrogai/scratch into .temp..."); + log.info("📦 Cloning pullfrogai/scratch into .temp..."); execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" }); } } @@ -42,7 +43,7 @@ export function setupTestRepo(options: SetupOptions): void { * Setup git configuration to avoid identity errors */ export function setupGitConfig(): void { - console.log("🔧 Setting up git configuration..."); + log.info("🔧 Setting up git configuration..."); execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" }); execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" }); } @@ -51,18 +52,18 @@ export function setupGitConfig(): void { * Setup git authentication using GitHub token */ export function setupGitAuth(githubToken: string, repoContext: RepoContext): void { - console.log("🔐 Setting up git authentication..."); + log.info("🔐 Setting up git authentication..."); // Remove existing git auth headers that actions/checkout might have set try { execSync("git config --unset-all http.https://github.com/.extraheader", { stdio: "inherit" }); - console.log("✓ Removed existing authentication headers"); + log.info("✓ Removed existing authentication headers"); } catch { - console.log("No existing authentication headers to remove"); + log.info("No existing authentication headers to remove"); } // Update remote URL to embed the token const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`; execSync(`git remote set-url origin "${remoteUrl}"`, { stdio: "inherit" }); - console.log("✓ Updated remote URL with authentication token"); + log.info("✓ Updated remote URL with authentication token"); }