Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d91473f6e | |||
| 3937c3bdba | |||
| bac3f3e9c6 | |||
| 5ea1d95b70 | |||
| 6d0c21f0f5 | |||
| c31824144b | |||
| 0a63f3da9d |
+1
-1
@@ -7,4 +7,4 @@ echo "🔨 Building action..."
|
|||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
# Add the built files and lockfile to the commit
|
# Add the built files and lockfile to the commit
|
||||||
git add entry.js pnpm-lock.yaml
|
git add entry.js mcp-server.js pnpm-lock.yaml
|
||||||
|
|||||||
+100
-1
@@ -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 { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import packageJson from "../package.json" with { type: "json" };
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { type Agent, instructions } from "./shared.ts";
|
import { type Agent, instructions } from "./shared.ts";
|
||||||
|
|
||||||
|
let cachedCliPath: string | undefined;
|
||||||
|
|
||||||
export const claude: Agent = {
|
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 }) => {
|
run: async ({ prompt, mcpServers, apiKey }) => {
|
||||||
process.env.ANTHROPIC_API_KEY = apiKey;
|
process.env.ANTHROPIC_API_KEY = apiKey;
|
||||||
|
|
||||||
|
if (!cachedCliPath) {
|
||||||
|
throw new Error("Claude CLI not installed. Call install() before run().");
|
||||||
|
}
|
||||||
|
|
||||||
const queryInstance = query({
|
const queryInstance = query({
|
||||||
prompt: `${instructions}\n\n${prompt}`,
|
prompt: `${instructions}\n\n****** USER PROMPT ******\n${prompt}`,
|
||||||
options: {
|
options: {
|
||||||
permissionMode: "bypassPermissions",
|
permissionMode: "bypassPermissions",
|
||||||
mcpServers,
|
mcpServers,
|
||||||
|
pathToClaudeCodeExecutable: cachedCliPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,4 +239,6 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
},
|
},
|
||||||
system: () => {},
|
system: () => {},
|
||||||
stream_event: () => {},
|
stream_event: () => {},
|
||||||
|
tool_progress: () => {},
|
||||||
|
auth_status: () => {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface AgentConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Agent = {
|
export type Agent = {
|
||||||
|
install: () => Promise<string>;
|
||||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,17 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { type Inputs, main } from "./main.ts";
|
import { type Inputs, main } from "./main.ts";
|
||||||
import { createMcpServer } from "./mcp/server.ts";
|
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> {
|
async function run(): Promise<void> {
|
||||||
|
// 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 {
|
try {
|
||||||
const inputs: Inputs = {
|
const inputs: Inputs = {
|
||||||
prompt: core.getInput("prompt", { required: true }),
|
prompt: core.getInput("prompt", { required: true }),
|
||||||
|
|||||||
+18
-7
@@ -1,15 +1,11 @@
|
|||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
|
|
||||||
// Build the GitHub Action bundle only
|
const sharedConfig = {
|
||||||
// For npm package builds, use zshy (pnpm build:npm)
|
|
||||||
await build({
|
|
||||||
entryPoints: ["./entry.ts"],
|
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: "./entry.js",
|
|
||||||
format: "esm",
|
format: "esm",
|
||||||
platform: "node",
|
platform: "node",
|
||||||
target: "node20",
|
target: "node20",
|
||||||
minify: true,
|
minify: false,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
// Bundle all dependencies - GitHub Actions doesn't have node_modules
|
// Bundle all dependencies - GitHub Actions doesn't have node_modules
|
||||||
// Only mark optional peer dependencies as external
|
// Only mark optional peer dependencies as external
|
||||||
@@ -19,13 +15,28 @@ await build({
|
|||||||
"sury",
|
"sury",
|
||||||
],
|
],
|
||||||
// Provide a proper require shim for CommonJS modules bundled into ESM
|
// Provide a proper require shim for CommonJS modules bundled into ESM
|
||||||
|
// We use a unique variable name to avoid conflicts with bundled imports
|
||||||
banner: {
|
banner: {
|
||||||
js: `import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);`,
|
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
|
// Enable tree-shaking to remove unused code
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
// Drop console statements in production (but keep for debugging)
|
// Drop console statements in production (but keep for debugging)
|
||||||
drop: [],
|
drop: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the main entry bundle (without MCP)
|
||||||
|
await build({
|
||||||
|
...sharedConfig,
|
||||||
|
entryPoints: ["./entry.ts"],
|
||||||
|
outfile: "./entry.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the MCP server bundle
|
||||||
|
await build({
|
||||||
|
...sharedConfig,
|
||||||
|
entryPoints: ["./mcp/server.ts"],
|
||||||
|
outfile: "./mcp-server.js",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Build completed successfully!");
|
console.log("✅ Build completed successfully!");
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
try {
|
try {
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
|
|
||||||
// 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()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupGitConfig();
|
setupGitConfig();
|
||||||
|
|
||||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||||
@@ -44,6 +36,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
|
|
||||||
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
||||||
|
|
||||||
|
// Install Claude CLI before running
|
||||||
|
await claude.install();
|
||||||
|
|
||||||
log.info("Running Claude Agent SDK...");
|
log.info("Running Claude Agent SDK...");
|
||||||
log.box(inputs.prompt, { title: "Prompt" });
|
log.box(inputs.prompt, { title: "Prompt" });
|
||||||
|
|
||||||
|
|||||||
Executable
+101715
File diff suppressed because one or more lines are too long
+25
-9
@@ -2,7 +2,11 @@
|
|||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readdirSync } from "node:fs";
|
||||||
|
import { dirname } from "node:path";
|
||||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import { fromHere } from "@ark/fs";
|
||||||
|
import { log } from "../utils/cli.ts";
|
||||||
import { parseRepoContext } from "../utils/github.ts";
|
import { parseRepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
export const ghPullfrogMcpName = "gh-pullfrog";
|
export const ghPullfrogMcpName = "gh-pullfrog";
|
||||||
@@ -15,19 +19,31 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
|||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||||
|
|
||||||
// Get absolute path to entry.js - use GITHUB_ACTION_PATH if available, otherwise current directory
|
// In production (GitHub Actions), mcp-server.js is in same directory as entry.js (where this is bundled)
|
||||||
const entryPath = process.env.GITHUB_ACTION_PATH
|
// In development, server.ts is in the same directory as this file (config.ts)
|
||||||
? `${process.env.GITHUB_ACTION_PATH}/entry.js`
|
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server.js") : fromHere("server.ts");
|
||||||
: `${process.cwd()}/entry.js`;
|
|
||||||
|
log.info(`MCP Server Path: ${serverPath}`);
|
||||||
|
const pathExists = existsSync(serverPath);
|
||||||
|
log.info(`MCP Server Path exists: ${pathExists}`);
|
||||||
|
|
||||||
|
if (!pathExists) {
|
||||||
|
const dir = dirname(serverPath);
|
||||||
|
log.info(`Directory: ${dir}`);
|
||||||
|
try {
|
||||||
|
const files = readdirSync(dir);
|
||||||
|
log.info(`Files in directory: ${files.join(", ")}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.warning(
|
||||||
|
`Failed to read directory: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[ghPullfrogMcpName]: {
|
[ghPullfrogMcpName]: {
|
||||||
command: "node",
|
command: "node",
|
||||||
args: [
|
args: [serverPath],
|
||||||
"--input-type=module",
|
|
||||||
"-e",
|
|
||||||
`import('${entryPath.replace(/'/g, "\\'")}').then(m => m.createMcpServer())`,
|
|
||||||
],
|
|
||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
GITHUB_REPOSITORY: githubRepository,
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
|||||||
+13
-15
@@ -8,20 +8,18 @@ import { PullRequestInfoTool } from "./prInfo.ts";
|
|||||||
import { ReviewTool } from "./review.ts";
|
import { ReviewTool } from "./review.ts";
|
||||||
import { addTools } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
export function createMcpServer(): void {
|
const server = new FastMCP({
|
||||||
const server = new FastMCP({
|
name: "gh-pullfrog",
|
||||||
name: "gh-pullfrog",
|
version: "0.0.1",
|
||||||
version: "0.0.1",
|
});
|
||||||
});
|
|
||||||
|
|
||||||
addTools(server, [
|
addTools(server, [
|
||||||
CreateCommentTool,
|
CreateCommentTool,
|
||||||
EditCommentTool,
|
EditCommentTool,
|
||||||
IssueTool,
|
IssueTool,
|
||||||
PullRequestTool,
|
PullRequestTool,
|
||||||
ReviewTool,
|
ReviewTool,
|
||||||
PullRequestInfoTool,
|
PullRequestInfoTool,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
server.start();
|
server.start();
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.80",
|
"version": "0.0.87",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function setupGitConfig(): void {
|
|||||||
// Only set up git config in GitHub Actions environment
|
// Only set up git config in GitHub Actions environment
|
||||||
// In local development, use the user's existing git config
|
// In local development, use the user's existing git config
|
||||||
if (!process.env.GITHUB_ACTIONS) {
|
if (!process.env.GITHUB_ACTIONS) {
|
||||||
log.warning("Skipping git configuration setup (not in GitHub Actions)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ export function setupGitAuth(githubToken: string, repoContext: RepoContext): voi
|
|||||||
// Only set up git auth in GitHub Actions environment
|
// Only set up git auth in GitHub Actions environment
|
||||||
// In local testing, this would overwrite the real git remote with fake credentials
|
// In local testing, this would overwrite the real git remote with fake credentials
|
||||||
if (!process.env.GITHUB_ACTIONS) {
|
if (!process.env.GITHUB_ACTIONS) {
|
||||||
log.warning("Skipping git authentication setup (not in GitHub Actions)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user