Compare commits

...

10 Commits

Author SHA1 Message Date
David Blass 5ea1d95b70 debug dir structure 2025-11-06 20:38:20 -05:00
David Blass 6d0c21f0f5 move directory logging 2025-11-06 20:34:35 -05:00
David Blass c31824144b fix bundle import 2025-11-06 20:32:28 -05:00
David Blass 0a63f3da9d try download claude 2025-11-06 20:28:58 -05:00
David Blass 42b023cc86 okok 2025-11-06 19:37:31 -05:00
David Blass 854e3d5e4d add debug 2025-11-06 19:19:26 -05:00
David Blass 5bb1b779a8 iter 2025-11-06 19:13:42 -05:00
David Blass 599264694e try again 2025-11-06 19:08:25 -05:00
David Blass b9c15e9f38 fix github config 2025-11-06 19:05:57 -05:00
Pullfrog Action 7ef44eb254 try esm action 2025-11-06 19:03:19 -05:00
13 changed files with 114969 additions and 429 deletions
+1 -4
View File
@@ -1,6 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Ensure lockfile is up to date
echo "🔒 Updating lockfile..."
pnpm install --lockfile-only
@@ -10,4 +7,4 @@ echo "🔨 Building action..."
pnpm build
# Add the built files and lockfile to the commit
git add entry.cjs pnpm-lock.yaml
git add entry.js pnpm-lock.yaml
+1 -1
View File
@@ -13,7 +13,7 @@ inputs:
runs:
using: "node20"
main: "entry.cjs"
main: "entry.js"
branding:
icon: "code"
+100 -1
View File
@@ -1,16 +1,113 @@
import { execSync } from "node:child_process";
import { createWriteStream, existsSync, rmSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { type Agent, instructions } from "./shared.ts";
let cachedCliPath: string | undefined;
export const claude: Agent = {
install: async (): Promise<string> => {
if (cachedCliPath) {
log.info(`Using cached Claude Code CLI at ${cachedCliPath}`);
return cachedCliPath;
}
// Get the SDK version from package.json and resolve to actual version
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
let sdkVersion: string;
// If it's a range (starts with ^ or ~), query npm registry for the latest matching version
if (versionRange.startsWith("^") || versionRange.startsWith("~")) {
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
log.info(`Resolving version for range ${versionRange}...`);
try {
const registryResponse = await fetch(`${npmRegistry}/@anthropic-ai/claude-agent-sdk`);
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
// Get the latest version that matches the range (simplified: just use latest)
sdkVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${sdkVersion}`);
} catch (error) {
log.warning(
`Failed to resolve version from registry, using latest: ${error instanceof Error ? error.message : String(error)}`
);
sdkVersion = "latest";
}
} else {
sdkVersion = versionRange;
}
log.info(`📦 Installing Claude Code CLI from @anthropic-ai/claude-agent-sdk@${sdkVersion}...`);
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), "claude-cli-"));
const tarballPath = join(tempDir, "package.tgz");
try {
// Download tarball from npm
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
const tarballUrl = `${npmRegistry}/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-${sdkVersion}.tgz`;
log.info(`Downloading from ${tarballUrl}...`);
const response = await fetch(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
// Write tarball to file
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body as any, fileStream);
log.info(`Downloaded tarball to ${tarballPath}`);
// Extract tarball
log.info(`Extracting tarball...`);
execSync(`tar -xzf "${tarballPath}" -C "${tempDir}"`, { stdio: "pipe" });
// Find cli.js in the extracted package
const extractedDir = join(tempDir, "package");
const cliPath = join(extractedDir, "cli.js");
if (!existsSync(cliPath)) {
throw new Error(`cli.js not found in extracted package at ${cliPath}`);
}
cachedCliPath = cliPath;
log.info(`✓ Claude Code CLI installed at ${cliPath}`);
return cliPath;
} catch (error) {
// Cleanup on error
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
throw error;
}
},
run: async ({ prompt, mcpServers, apiKey }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
if (!cachedCliPath) {
throw new Error("Claude CLI not installed. Call install() before run().");
}
const queryInstance = query({
prompt: `${instructions}\n\n${prompt}`,
prompt: `${instructions}\n\n****** USER PROMPT ******\n${prompt}`,
options: {
permissionMode: "bypassPermissions",
mcpServers,
pathToClaudeCodeExecutable: cachedCliPath,
},
});
@@ -142,4 +239,6 @@ const messageHandlers: SDKMessageHandlers = {
},
system: () => {},
stream_event: () => {},
tool_progress: () => {},
auth_status: () => {},
};
+1
View File
@@ -22,6 +22,7 @@ export interface AgentConfig {
}
export type Agent = {
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>;
};
-389
View File
File diff suppressed because one or more lines are too long
Executable
+114767
View File
File diff suppressed because one or more lines are too long
+50 -19
View File
@@ -4,27 +4,65 @@
* Entry point for GitHub Action
*/
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import * as core from "@actions/core";
import { type } from "arktype";
import { Inputs, main } from "./main.ts";
import { dirName } from "@ark/fs";
import { type Inputs, main } from "./main.ts";
import { createMcpServer } from "./mcp/server.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts";
// Export createMcpServer so it can be called from the spawned MCP process
export { createMcpServer };
async function run(): Promise<void> {
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
async function printDirectoryTree(dir: string, prefix = "", rootDir = dir): Promise<string> {
const entries = await readdir(dir, { withFileTypes: true });
const lines: string[] = [];
const inputsJson = process.env.INPUTS_JSON;
if (!inputsJson) {
throw new Error("INPUTS_JSON environment variable not found");
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const isLast = i === entries.length - 1;
const currentPrefix = isLast ? "└── " : "├── ";
const nextPrefix = isLast ? " " : "│ ";
const fullPath = join(dir, entry.name);
lines.push(`${prefix}${currentPrefix}${entry.name}`);
if (entry.isDirectory()) {
const subTree = await printDirectoryTree(fullPath, `${prefix}${nextPrefix}`, rootDir);
lines.push(subTree);
}
}
const parsed = type("string.json.parse").assert(inputsJson);
const inputs = Inputs.assert(parsed);
return lines.join("\n");
}
async function run(): Promise<void> {
// Debug: Print current directory tree before changing directories
const actionDir = dirName();
log.info(`action dir: ${actionDir}`);
try {
const tree = await printDirectoryTree(actionDir);
log.info(`Directory tree:\n${tree}`);
} catch (error) {
log.warning(
`Failed to print directory tree: ${error instanceof Error ? error.message : String(error)}`
);
}
// Change to GITHUB_WORKSPACE if set (this is where actions/checkout puts the repo)
// JavaScript actions run from the action's directory, not the checked-out repo
if (process.env.GITHUB_WORKSPACE && process.cwd() !== process.env.GITHUB_WORKSPACE) {
log.debug(`Changing to GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
process.chdir(process.env.GITHUB_WORKSPACE);
log.debug(`New working directory: ${process.cwd()}`);
}
try {
const inputs: Inputs = {
prompt: core.getInput("prompt", { required: true }),
anthropic_api_key: core.getInput("anthropic_api_key") || undefined,
};
const result = await main(inputs);
@@ -37,11 +75,4 @@ async function run(): Promise<void> {
}
}
// Only run main action if INPUTS_JSON is set (running as GitHub Action)
// When SDK spawns MCP server, it calls createMcpServer() directly
if (process.env.INPUTS_JSON) {
// Wrap in IIFE to avoid top-level await in CJS
(async () => {
await run();
})();
}
await run();
+10 -4
View File
@@ -5,18 +5,24 @@ import { build } from "esbuild";
await build({
entryPoints: ["./entry.ts"],
bundle: true,
outfile: "./entry.cjs",
format: "cjs",
outfile: "./entry.js",
format: "esm",
platform: "node",
target: "node20",
minify: true,
minify: false,
sourcemap: false,
// Mark optional peer dependencies as external to avoid bundling errors
// Bundle all dependencies - GitHub Actions doesn't have node_modules
// Only mark optional peer dependencies as external
external: [
"@valibot/to-json-schema",
"effect",
"sury",
],
// Provide a proper require shim for CommonJS modules bundled into ESM
// We use a unique variable name to avoid conflicts with bundled imports
banner: {
js: `import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __dirnameFn } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirnameFn(__filename);`,
},
// Enable tree-shaking to remove unused code
treeShaking: true,
// Drop console statements in production (but keep for debugging)
+5 -1
View File
@@ -1,6 +1,7 @@
import { type } from "arktype";
import { claude } from "./agents/claude.ts";
import { createMcpConfigs } from "./mcp/config.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts";
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
@@ -22,7 +23,7 @@ export type PromptJSON = {};
export async function main(inputs: Inputs): Promise<MainResult> {
try {
log.info("Starting agent run...");
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
setupGitConfig();
@@ -35,6 +36,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
// Install Claude CLI before running
await claude.install();
log.info("Running Claude Agent SDK...");
log.box(inputs.prompt, { title: "Prompt" });
+8 -4
View File
@@ -15,15 +15,19 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
// Get absolute path to entry.cjs - use GITHUB_ACTION_PATH if available, otherwise current directory
// Get absolute path to entry.js - use GITHUB_ACTION_PATH if available, otherwise current directory
const entryPath = process.env.GITHUB_ACTION_PATH
? `${process.env.GITHUB_ACTION_PATH}/entry.cjs`
: `${process.cwd()}/entry.cjs`;
? `${process.env.GITHUB_ACTION_PATH}/entry.js`
: `${process.cwd()}/entry.js`;
return {
[ghPullfrogMcpName]: {
command: "node",
args: ["-e", `require('${entryPath.replace(/'/g, "\\'")}').createMcpServer()`],
args: [
"--input-type=module",
"-e",
`import('${entryPath.replace(/'/g, "\\'")}').then(m => m.createMcpServer())`,
],
env: {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.75",
"version": "0.0.84",
"type": "module",
"files": [
"index.js",
-3
View File
@@ -5,7 +5,6 @@ import { fromHere } from "@ark/fs";
import arg from "arg";
import { config } from "dotenv";
import { type Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts";
import { setupTestRepo } from "./utils/setup.ts";
@@ -15,8 +14,6 @@ export async function run(
prompt: string
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const tempDir = join(process.cwd(), ".temp");
setupTestRepo({ tempDir, forceClean: true });
+25 -2
View File
@@ -41,17 +41,40 @@ export function setupTestRepo(options: SetupOptions): void {
/**
* Setup git configuration to avoid identity errors
* Only runs in GitHub Actions environment to avoid overwriting local git config
*/
export function setupGitConfig(): void {
// Only set up git config in GitHub Actions environment
// In local development, use the user's existing git config
if (!process.env.GITHUB_ACTIONS) {
return;
}
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" });
try {
execSync('git config user.email "action@pullfrog.ai"', { stdio: "pipe" });
execSync('git config user.name "Pullfrog Action"', { stdio: "pipe" });
log.debug("setupGitConfig: ✓ Git configuration set successfully");
} catch (error) {
// If git config fails, log warning but don't fail the action
// This can happen if we're not in a git repo or git isn't available
log.warning(
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Setup git authentication using GitHub token
* Only runs in GitHub Actions environment to avoid breaking local git remotes
*/
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
// Only set up git auth in GitHub Actions environment
// In local testing, this would overwrite the real git remote with fake credentials
if (!process.env.GITHUB_ACTIONS) {
return;
}
log.info("🔐 Setting up git authentication...");
// Remove existing git auth headers that actions/checkout might have set