Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1328894afd | |||
| 85731f8360 | |||
| 1922352d86 | |||
| c0f31415a3 |
+3
-2
@@ -32,9 +32,10 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ${{ github.action_path }}
|
working-directory: ${{ github.action_path }}
|
||||||
- name: Run agent
|
- name: Run agent
|
||||||
run: node entry.ts
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
node ${{ github.action_path }}/entry.ts
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ${{ github.action_path }}
|
|
||||||
env:
|
env:
|
||||||
INPUTS_JSON: ${{ toJSON(inputs) }}
|
INPUTS_JSON: ${{ toJSON(inputs) }}
|
||||||
|
|
||||||
|
|||||||
+21
-10
@@ -1,6 +1,7 @@
|
|||||||
import { access, constants } from "node:fs/promises";
|
import { access, constants } from "node:fs/promises";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { createMcpConfig } from "../mcp/config.ts";
|
import { createMcpConfig } from "../mcp/config.ts";
|
||||||
|
import { debugLog, isDebug } from "../utils/logging.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import { boxString, tableString } from "../utils/table.ts";
|
import { boxString, tableString } from "../utils/table.ts";
|
||||||
import { instructions } from "./shared.ts";
|
import { instructions } from "./shared.ts";
|
||||||
@@ -77,24 +78,27 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
ANTHROPIC_API_KEY: this.apiKey,
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
GITHUB_TOKEN: this.githubInstallationToken,
|
...(isDebug() && { LOG_LEVEL: "debug" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
|
|
||||||
const mcpConfig = createMcpConfig(this.githubInstallationToken);
|
const mcpConfig = createMcpConfig(this.githubInstallationToken);
|
||||||
console.log("📋 MCP Config:", mcpConfig);
|
|
||||||
|
if (isDebug()) {
|
||||||
|
debugLog(`📋 MCP Config: ${mcpConfig}`);
|
||||||
|
}
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"--print",
|
"--print",
|
||||||
"--output-format",
|
"--output-format",
|
||||||
"stream-json",
|
"stream-json",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"--debug",
|
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"bypassPermissions",
|
"bypassPermissions",
|
||||||
"--mcp-config",
|
"--mcp-config",
|
||||||
mcpConfig,
|
mcpConfig,
|
||||||
|
...(isDebug() ? ["--debug"] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
core.startGroup("🔄 Run details");
|
core.startGroup("🔄 Run details");
|
||||||
@@ -115,7 +119,9 @@ export class ClaudeAgent implements Agent {
|
|||||||
input: `${instructions} ${prompt}`,
|
input: `${instructions} ${prompt}`,
|
||||||
timeout: 10 * 60 * 1000, // 10 minutes
|
timeout: 10 * 60 * 1000, // 10 minutes
|
||||||
onStdout: (_chunk) => {
|
onStdout: (_chunk) => {
|
||||||
processJSONChunk(_chunk, this);
|
if (_chunk.trim()) {
|
||||||
|
processJSONChunk(_chunk, this);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onStderr: (_chunk) => {
|
onStderr: (_chunk) => {
|
||||||
if (_chunk.trim()) {
|
if (_chunk.trim()) {
|
||||||
@@ -166,14 +172,19 @@ export class ClaudeAgent implements Agent {
|
|||||||
*/
|
*/
|
||||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||||
try {
|
try {
|
||||||
// Skip debug lines that start with [DEBUG] or [debug]
|
|
||||||
const trimmedChunk = chunk.trim();
|
const trimmedChunk = chunk.trim();
|
||||||
if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) {
|
if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) {
|
||||||
console.log(chunk);
|
console.log(chunk);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chunk);
|
if (trimmedChunk.startsWith("[ERROR]") || trimmedChunk.startsWith("[error]")) {
|
||||||
|
console.error(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(trimmedChunk);
|
||||||
|
|
||||||
const parsedChunk = JSON.parse(trimmedChunk);
|
const parsedChunk = JSON.parse(trimmedChunk);
|
||||||
|
|
||||||
switch (parsedChunk.type) {
|
switch (parsedChunk.type) {
|
||||||
@@ -310,11 +321,11 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
debugLog(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.debug(`Failed to parse chunk: ${error}`);
|
debugLog(`Failed to parse chunk: ${error}`);
|
||||||
core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
debugLog(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { ClaudeAgent } from "./agents/claude.ts";
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
import { setupGitHubInstallationToken, parseRepoContext } from "./utils/github.ts";
|
||||||
|
import { setupGitConfig, setupGitAuth } from "./utils/setup.ts";
|
||||||
|
|
||||||
export const Inputs = type({
|
export const Inputs = type({
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
@@ -20,8 +21,12 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
try {
|
try {
|
||||||
core.info(`→ Starting agent run with Claude Code`);
|
core.info(`→ Starting agent run with Claude Code`);
|
||||||
|
|
||||||
// Setup GitHub installation token
|
setupGitConfig();
|
||||||
|
|
||||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
|
setupGitAuth(githubInstallationToken, repoContext);
|
||||||
|
|
||||||
const agent = new ClaudeAgent({
|
const agent = new ClaudeAgent({
|
||||||
apiKey: inputs.anthropic_api_key!,
|
apiKey: inputs.anthropic_api_key!,
|
||||||
|
|||||||
+4
-7
@@ -2,18 +2,15 @@
|
|||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
import { fromHere } from "@ark/fs";
|
import { fromHere } from "@ark/fs";
|
||||||
|
import { parseRepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
const actionPath = fromHere("..");
|
const actionPath = fromHere("..");
|
||||||
|
|
||||||
export const mcpServerName = "gh-pullfrog";
|
export const mcpServerName = "gh-pullfrog";
|
||||||
|
|
||||||
export function createMcpConfig(githubInstallationToken: string) {
|
export function createMcpConfig(githubInstallationToken: string) {
|
||||||
const githubRepository = process.env.GITHUB_REPOSITORY;
|
const repoContext = parseRepoContext();
|
||||||
if (!githubRepository) {
|
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||||
throw new Error(
|
|
||||||
"GITHUB_REPOSITORY environment variable is required for MCP GitHub integration"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
@@ -24,7 +21,7 @@ export function createMcpConfig(githubInstallationToken: string) {
|
|||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
GITHUB_REPOSITORY: githubRepository,
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
LOG_LEVEL: "debug",
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface ToolResult {
|
|||||||
type: "text";
|
type: "text";
|
||||||
text: string;
|
text: string;
|
||||||
}[];
|
}[];
|
||||||
error?: string;
|
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +71,6 @@ const handleToolError = (error: unknown): ToolResult => {
|
|||||||
text: `Error: ${errorMessage}`,
|
text: `Error: ${errorMessage}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
error: errorMessage,
|
|
||||||
isError: true,
|
isError: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.55",
|
"version": "0.0.60",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+1
-4
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { buildAction, setupTestRepo } from "./setup.ts";
|
import { setupTestRepo } from "./setup.ts";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -19,8 +19,6 @@ export function runAct(prompt: string): void {
|
|||||||
|
|
||||||
config({ path: envPath });
|
config({ path: envPath });
|
||||||
|
|
||||||
buildAction(actionPath);
|
|
||||||
|
|
||||||
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||||
|
|
||||||
const distPath = join(actionPath, ".act-dist");
|
const distPath = join(actionPath, ".act-dist");
|
||||||
@@ -54,7 +52,6 @@ export function runAct(prompt: string): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const actCommand = actCommandParts.join(" ");
|
const actCommand = actCommandParts.join(" ");
|
||||||
|
|
||||||
console.log("🚀 Running act with prompt:");
|
console.log("🚀 Running act with prompt:");
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-7
@@ -1,5 +1,6 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { existsSync, rmSync } from "node:fs";
|
import { existsSync, rmSync } from "node:fs";
|
||||||
|
import type { RepoContext } from "./github.ts";
|
||||||
|
|
||||||
export interface SetupOptions {
|
export interface SetupOptions {
|
||||||
tempDir: string;
|
tempDir: string;
|
||||||
@@ -38,12 +39,30 @@ export function setupTestRepo(options: SetupOptions): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the action bundles
|
* Setup git configuration to avoid identity errors
|
||||||
*/
|
*/
|
||||||
export function buildAction(actionPath: string): void {
|
export function setupGitConfig(): void {
|
||||||
console.log("🔨 Building fresh bundles with esbuild...");
|
console.log("🔧 Setting up git configuration...");
|
||||||
execSync("node esbuild.config.js", {
|
execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" });
|
||||||
cwd: actionPath,
|
execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" });
|
||||||
stdio: "inherit",
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Setup git authentication using GitHub token
|
||||||
|
*/
|
||||||
|
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
|
||||||
|
console.log("🔐 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");
|
||||||
|
} catch {
|
||||||
|
console.log("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");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ export interface SpawnOptions {
|
|||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
input?: string;
|
input?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
cwd?: string;
|
||||||
onStdout?: (chunk: string) => void;
|
onStdout?: (chunk: string) => void;
|
||||||
onStderr?: (chunk: string) => void;
|
onStderr?: (chunk: string) => void;
|
||||||
}
|
}
|
||||||
@@ -21,7 +22,7 @@ export interface SpawnResult {
|
|||||||
* Spawn a subprocess with streaming callbacks and buffered results
|
* Spawn a subprocess with streaming callbacks and buffered results
|
||||||
*/
|
*/
|
||||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||||
const { cmd, args, env, input, timeout, onStdout, onStderr } = options;
|
const { cmd, args, env, input, timeout, cwd, onStdout, onStderr } = options;
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
@@ -31,6 +32,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
const child = nodeSpawn(cmd, args, {
|
const child = nodeSpawn(cmd, args, {
|
||||||
env: env ? { ...process.env, ...env } : process.env,
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
cwd: cwd || process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user