This commit is contained in:
Shawn Morreau
2025-11-26 15:14:21 -05:00
16 changed files with 76701 additions and 111145 deletions
+1 -1
View File
@@ -8,5 +8,5 @@ if git diff --cached --name-only | grep -q "^package.json$"; then
pnpm build
# Add the built files and lockfile to the commit
git add entry mcp-server pnpm-lock.yaml
git add entry pnpm-lock.yaml
fi
-299
View File
@@ -1,299 +0,0 @@
# Claude Code Action Architecture & Flow
This document provides a comprehensive overview of how the official (Anthropic) Claude Code Action works, from token exchange through post-run cleanup.
## Overview
The Claude Code Action is a sophisticated GitHub automation platform that enables Claude to interact with GitHub repositories through secure token exchange, intelligent mode detection, and comprehensive GitHub API integration.
## High-Level Architecture
```mermaid
graph TD
Start([GitHub Action Triggered]) --> Setup[Setup Environment<br/>- Install Bun<br/>- Install Dependencies]
Setup --> ParseContext[Parse GitHub Context<br/>- Extract event data<br/>- Parse inputs]
ParseContext --> ModeDetection{Mode Detection}
ModeDetection -->|Has explicit prompt| AgentMode[AGENT MODE<br/>Direct automation]
ModeDetection -->|@claude mention/assignment/label| TagMode[TAG MODE<br/>Interactive response]
ModeDetection -->|No trigger| DefaultAgent[Default to Agent<br/>(won't trigger)]
%% Token Exchange Branch
AgentMode --> TokenExchange[Token Exchange Process]
TagMode --> TokenExchange
TokenExchange --> TokenMethod{Token Method}
TokenMethod -->|Custom token provided| UseCustom[Use Custom GitHub Token]
TokenMethod -->|No custom token| OIDC[Generate OIDC Token<br/>core.getIDToken()]
OIDC --> Exchange[Exchange OIDC for App Token<br/>api.anthropic.com/api/github/github-app-token-exchange]
Exchange --> CreateOctokit[Create Authenticated Octokit Client<br/>REST + GraphQL]
UseCustom --> CreateOctokit
%% Permission Checks
CreateOctokit --> PermCheck[Check Write Permissions<br/>Only for entity contexts]
PermCheck -->|No permissions| PermFail[❌ Exit: No write access]
PermCheck -->|Has permissions| TriggerCheck{Check Trigger Conditions}
%% Trigger Validation
TriggerCheck -->|Agent Mode| AgentTrigger{Has explicit prompt?}
TriggerCheck -->|Tag Mode| TagTrigger{Contains @claude mention<br/>or assignment/label?}
AgentTrigger -->|No prompt| NoTrigger[❌ Skip: No trigger found]
AgentTrigger -->|Has prompt| PrepareAgent[Prepare Agent Mode]
TagTrigger -->|No mention| NoTrigger
TagTrigger -->|Has mention| PrepareTag[Prepare Tag Mode]
%% Mode-Specific Preparation
PrepareAgent --> AgentPrep[Agent Mode Preparation<br/>- Create prompt file<br/>- Setup MCP servers<br/>- No tracking comment]
PrepareTag --> TagPrep[Tag Mode Preparation<br/>- Create tracking comment<br/>- Setup branches<br/>- Fetch GitHub data<br/>- Setup MCP servers]
%% Data Fetching (Tag Mode)
TagPrep --> DataFetch[Fetch GitHub Data<br/>GraphQL + REST API]
DataFetch --> FetchWhat{What to fetch?}
FetchWhat -->|Pull Request| PRData[PR Data:<br/>- Comments & reviews<br/>- Changed files + SHAs<br/>- Commit history<br/>- Author info]
FetchWhat -->|Issue| IssueData[Issue Data:<br/>- Comments<br/>- Issue details<br/>- Author info]
PRData --> ProcessImages[Process Images<br/>Download & convert to base64]
IssueData --> ProcessImages
ProcessImages --> SetupBranch[Setup Branch<br/>- Create Claude branch<br/>- Configure git auth]
%% MCP Server Setup
AgentPrep --> MCPSetup[Setup MCP Servers]
SetupBranch --> MCPSetup
MCPSetup --> MCPServers{MCP Servers}
MCPServers --> GitHubActions[GitHub Actions Server<br/>- Workflow data<br/>- CI results]
MCPServers --> GitHubComments[GitHub Comment Server<br/>- Comment operations]
MCPServers --> GitHubFiles[GitHub File Ops Server<br/>- File operations<br/>- Branch management]
MCPServers --> GitHubInline[GitHub Inline Comment Server<br/>- PR review comments]
GitHubActions --> PromptGen[Generate Prompt]
GitHubComments --> PromptGen
GitHubFiles --> PromptGen
GitHubInline --> PromptGen
%% Prompt Generation
PromptGen --> PromptType{Prompt Type}
PromptType -->|Agent Mode| AgentPrompt[Agent Prompt:<br/>- Direct user prompt<br/>- Minimal context]
PromptType -->|Tag Mode| TagPrompt[Tag Prompt:<br/>- Rich GitHub context<br/>- PR/Issue details<br/>- Changed files<br/>- Comments & reviews<br/>- Commit instructions]
AgentPrompt --> ClaudeRun[Run Claude Code]
TagPrompt --> ClaudeRun
%% Claude Execution
ClaudeRun --> ClaudeExec[Claude Code Execution<br/>base-action/src/index.ts]
ClaudeExec --> ClaudeArgs[Prepare Claude Args<br/>- Prompt file path<br/>- Custom claude_args<br/>- Output format: stream-json]
ClaudeArgs --> ClaudeProvider{Provider}
ClaudeProvider -->|Default| AnthropicAPI[Anthropic API<br/>ANTHROPIC_API_KEY]
ClaudeProvider -->|Bedrock| AWSBedrock[AWS Bedrock<br/>OIDC + AWS credentials]
ClaudeProvider -->|Vertex| GCPVertex[GCP Vertex AI<br/>OIDC + GCP credentials]
AnthropicAPI --> ClaudeProcess[Spawn Claude Process<br/>- Named pipe for input<br/>- Stream JSON output]
AWSBedrock --> ClaudeProcess
GCPVertex --> ClaudeProcess
ClaudeProcess --> ClaudeTools[Claude Tool Usage<br/>- MCP tools<br/>- File operations<br/>- GitHub API calls<br/>- Bash commands]
ClaudeTools --> ClaudeOutput[Claude Output Processing<br/>- Capture execution log<br/>- Parse JSON stream<br/>- Extract metrics]
%% Post-Run Actions
ClaudeOutput --> PostRun{Post-Run Actions}
PostRun -->|Success| Success[✅ Success Path]
PostRun -->|Failure| Failure[❌ Failure Path]
Success --> UpdateComment[Update Tracking Comment<br/>- Job run link<br/>- Branch link<br/>- PR link (if created)<br/>- Execution metrics]
Failure --> UpdateComment
UpdateComment --> BranchCleanup[Branch Cleanup<br/>- Check for changes<br/>- Delete empty branches<br/>- Keep branches with commits]
BranchCleanup --> FormatReport[Format Execution Report<br/>- Parse conversation turns<br/>- Format tool usage<br/>- Add to GitHub step summary]
FormatReport --> RevokeToken[Revoke App Token<br/>DELETE /installation/token]
RevokeToken --> End([Action Complete])
```
## Key Components
### 1. Token Exchange Process
The action uses a secure OIDC token exchange system:
1. **OIDC Token Generation**: `core.getIDToken("claude-code-github-action")`
2. **Token Exchange**: POST to `https://api.anthropic.com/api/github/github-app-token-exchange`
3. **Authentication**: Creates authenticated Octokit clients for GitHub API access
**Security Benefits:**
- Repository-scoped access
- Time-limited tokens
- Permission-limited (only configured GitHub App permissions)
- Automatic token masking in logs
### 2. Mode Detection
The action automatically detects the appropriate execution mode:
#### **Agent Mode**
- **Trigger**: Explicit `prompt` input provided
- **Use Case**: Direct automation, custom workflows
- **Behavior**: Minimal context, direct execution
- **Tracking**: No tracking comments
#### **Tag Mode**
- **Trigger**: @claude mentions, issue assignments, or labels
- **Use Case**: Interactive GitHub responses
- **Behavior**: Rich context, comprehensive GitHub data
- **Tracking**: Creates and updates tracking comments
### 3. Data Fetching (Tag Mode)
When in Tag Mode, the action fetches comprehensive GitHub context:
#### **Pull Request Data:**
- Comments and reviews (including inline comments)
- Changed files with SHAs
- Commit history and metadata
- Author information
- File diff data
#### **Issue Data:**
- Issue details and metadata
- All comments
- Author information
- Labels and assignments
#### **Image Processing:**
- Downloads images from GitHub
- Converts to base64 for Claude
- Maps original URLs to processed content
### 4. MCP Server Integration
The action sets up multiple MCP (Model Context Protocol) servers to provide Claude with GitHub capabilities:
#### **GitHub Actions Server**
- Access to workflow runs and CI data
- Build status and test results
- Artifact information
#### **GitHub Comment Server**
- Comment creation and updates
- Issue and PR comment management
#### **GitHub File Operations Server**
- File reading and writing
- Branch creation and management
- Commit operations
#### **GitHub Inline Comment Server**
- PR review comment operations
- Line-specific feedback
### 5. Prompt Generation
The action generates context-rich prompts based on the detected mode:
#### **Agent Mode Prompts:**
- Direct user prompt
- Minimal GitHub context
- Focused on specific task
#### **Tag Mode Prompts:**
- Comprehensive GitHub context
- PR/Issue details and history
- Changed files and diffs
- Comment threads and reviews
- Commit instructions and guidelines
### 6. Claude Execution
The action runs Claude Code through multiple provider options:
#### **Provider Support:**
- **Anthropic API** (default): Direct API access with API key
- **AWS Bedrock**: OIDC authentication with AWS credentials
- **GCP Vertex AI**: OIDC authentication with GCP credentials
#### **Execution Process:**
1. **Named Pipe Setup**: Creates pipe for prompt input
2. **Process Spawning**: Spawns Claude Code process
3. **Stream Processing**: Captures JSON stream output
4. **Tool Integration**: Enables MCP tools and GitHub operations
### 7. Post-Run Actions
After Claude execution, the action performs comprehensive cleanup and reporting:
#### **Comment Updates:**
- Updates tracking comments with results
- Adds job run links and execution metrics
- Includes branch and PR links when created
#### **Branch Management:**
- Checks for actual changes in Claude branches
- Deletes empty branches to avoid clutter
- Preserves branches with meaningful commits
#### **Report Generation:**
- Parses execution logs and conversation turns
- Formats tool usage and results
- Adds formatted report to GitHub step summary
#### **Security Cleanup:**
- Revokes GitHub App installation token
- Cleans up temporary files and processes
## Security Considerations
### **Access Control:**
- Repository-scoped permissions only
- Write access validation for actors
- Bot user controls and allowlists
### **Token Management:**
- Short-lived installation tokens
- Automatic token revocation after use
- Secure OIDC-based exchange
### **Permission Boundaries:**
- Limited to configured GitHub App permissions
- No cross-repository access
- Scoped to specific repository operations
## Integration Points
### **With Pullfrog:**
The Claude Code Action can be integrated with Pullfrog's workflow system, providing:
- Standardized agent interaction patterns
- Consistent GitHub integration
- Reusable authentication flows
- Common MCP server infrastructure
### **With GitHub:**
- Native GitHub Actions integration
- Comprehensive API coverage (REST + GraphQL)
- Proper webhook handling
- Standard GitHub UI integration
## Development Notes
### **Key Files:**
- `src/entrypoints/prepare.ts`: Main preparation logic
- `src/modes/`: Mode detection and handling
- `src/github/token.ts`: OIDC token exchange
- `src/mcp/`: MCP server implementations
- `base-action/`: Core Claude Code execution
### **Testing:**
- Unit tests for individual components
- Integration tests for full workflows
- Local testing with `act` tool
- Comprehensive fixture support
This architecture provides a robust, secure, and extensible foundation for Claude-GitHub integration while maintaining clear separation of concerns and comprehensive error handling.
+127 -13
View File
@@ -1,21 +1,135 @@
# Pullfrog Action
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.ai/frog-white-200px.png">
<img src="https://pullfrog.ai/frog-200px.png" width="200px" align="center" alt="Pullfrog logo" />
</picture>
<h1 align="center">Pullfrog</h1>
<p align="center">
GitHub Action for running AI agents via Pullfrog to automate development workflows
<br/>
by <a href="https://pullfrog.ai">Pullfrog</a>
</p>
</p>
GitHub Action for running Claude Code and other agents via Pullfrog.
<br/>
> **📖 Claude Code Action Architecture**: For a detailed technical overview of how the Claude Code Action works (token exchange, modes, data fetching, execution flow), see [CLAUDE-ACTION.md](./CLAUDE-ACTION.md).
## What is Pullfrog?
## Quick Start
Pullfrog is a GitHub bot that brings the full power of your favorite coding agents into GitHub. It's open source and powered by GitHub Actions.
```bash
# Install dependencies
pnpm install
Once configured, you can start triggering agent runs.
- **Tag `@pullfrog`** — Tag `@pullfrog` in a comment anywhere in your repo. It will pull in any relevant context using the action's internal MCP server and perform the appropriate task.
- **Prompt from the web** — Trigger arbitrary tasks from the Pullfrog dashboard
- **Automated triggers** — Configure Pullfrog to trigger agent runs in response to specific events. Each of these triggers can be associated with custom prompt instructions.
- issue created
- issue labeled
- PR created
- PR review created
- PR review requested
- and more...
Pullfrog is the bridge between GitHub and your preferred coding agents and GitHub. Use it for:
- **🤖 Coding tasks** — Tell `@pullfrog` to implement something and it'll spin up a PR. If CI fails, it'll read the logs and attempt a fix automatically. It'll automatically address any PR reviews too.
- **🔍 PR review** — Coding agents are great at reviewing PRs. Using the "PR created" trigger, you can configure Pullfrog to auto-review new PRs.
- **🤙 Auto-respond to issues** — Via the "issue created" trigger, Pullfrog can automatically respond to common questions, create implementation plans, and link to related issues/PRs. Or (if you're feeling lucky) you can prompt it to immediately attempt a PR addressing new issues.
- **Literally whatever** — Want to have the agent automatically add docs to all new PRs? Cut a new release with agent-written notes on every commit to `main`? Pullfrog lets you do it.
<!-- Features
- **Agent-agnostic** — Switch between agents with the click of a radio button.
- ** -->
## Get started
Install the Pullfrog GitHub App on your personal or organization account. During installation you can choose to limit access to a specific repo or repos. After installation, you'll be redirected to the Pullfrog dashboard where you'll see an onboarding flow. This flow will create your `pullfrog.yml` workflow and prompt you to set up API keys. Once you finish those steps (2 minutes) you're ready to rock 🐸
[Add to GitHub ➜](https://github.com/apps/pullfrog/installations/new)
<details>
<summary><strong>Manual setup instructions</strong></summary>
You can also use the `pullfrog/action` Action without a GitHub App installation. This is more time-consuming to set up, and it places limitations on the actions your Agent will be capable of performing.
To manually set up the Pullfrog action, you need to set up two workflow files in your repository: `pullfrog.yml` (the execution logic) and `triggers.yml` (the event triggers).
#### 1. Create `pullfrog.yml`
Create a file at `.github/workflows/pullfrog.yml`. This is a reusable workflow that runs the Pullfrog action.
```yaml
name: Pullfrog
on:
workflow_dispatch:
inputs:
prompt:
type: string
description: "Agent prompt"
workflow_call:
inputs:
prompt:
description: "Agent prompt"
type: string
permissions:
id-token: write
contents: read
jobs:
pullfrog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run agent
uses: pullfrog/action@main # Use a specific version tag in production
with:
prompt: ${{ inputs.prompt }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Add other keys as needed:
# openai_api_key: ${{ secrets.OPENAI_API_KEY }}
```
## Testing with `play.ts`
#### 2. Create `triggers.yml`
```bash
pnpm play # Uses fixtures/play.txt
Create a file at `.github/workflows/triggers.yml`. This workflow listens for GitHub events and calls the `pullfrog.yml` workflow with the event data.
```yaml
name: Agent Triggers
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
# add other triggers as needed
jobs:
pullfrog:
# trigger conditions (e.g. only run if @pullfrog is mentioned)
if: contains(github.event.comment.body, '@pullfrog') || contains(github.event.issue.body, '@pullfrog')
permissions:
id-token: write
contents: write
issues: write
pull-requests: write
actions: read
checks: read
uses: ./.github/workflows/pullfrog.yml
with:
# pass the full event payload as the prompt
prompt: ${{ toJSON(github.event) }}
secrets: inherit
```
- Clones the scratch repository to `.temp`
- Runs Claude Code directly on your machine
- Fast iteration for development
</details>
+17 -21
View File
@@ -177,34 +177,30 @@ const messageHandlers: {
/**
* Configure MCP servers for Codex using the CLI.
* Codex CLI syntax: codex mcp add <name> --env KEY=value -- <command> [args...]
* For HTTP-based servers, use: codex mcp add <name> --url <url>
*/
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
const command = serverConfig.command;
const args = serverConfig.args || [];
const envVars = serverConfig.env || {};
if (serverConfig.type === "http") {
// HTTP-based MCP server - use --url flag
const addArgs = ["mcp", "add", serverName, "--url", serverConfig.url];
const addArgs = ["mcp", "add", serverName];
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
});
// Add environment variables as --env flags first
for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`);
}
addArgs.push("--", command, ...args);
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
});
if (addResult.status !== 0) {
if (addResult.status !== 0) {
throw new Error(
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
} else {
throw new Error(
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
`Unsupported MCP server type for Codex: ${(serverConfig as any).type || "unknown"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
}
}
+17 -1
View File
@@ -269,6 +269,22 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
const cursorConfigDir = join(realHome, ".cursor");
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
mkdirSync(cursorConfigDir, { recursive: true });
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
// Convert to Cursor's expected format (HTTP config)
const cursorMcpServers: Record<string, { type: string; url: string }> = {};
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (serverConfig.type !== "http") {
throw new Error(
`Unsupported MCP server type for Cursor: ${(serverConfig as any).type || "unknown"}`
);
}
cursorMcpServers[serverName] = {
type: "http",
url: serverConfig.url,
};
}
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: cursorMcpServers }, null, 2), "utf-8");
log.info(`MCP config written to ${mcpConfigPath}`);
}
+18 -18
View File
@@ -249,31 +249,31 @@ export const gemini = agent({
/**
* Configure MCP servers for Gemini using the CLI.
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --env KEY=value
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --transport <type>
* For HTTP-based servers, use: gemini mcp add <name> <url> --transport http
*/
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
const command = serverConfig.command;
const args = serverConfig.args || [];
const envVars = serverConfig.env || {};
if (serverConfig.type === "http") {
// HTTP-based MCP server - use URL with --transport http flag
const addArgs = ["mcp", "add", serverName, serverConfig.url, "--transport", "http"];
const addArgs = ["mcp", "add", serverName, command, ...args];
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
});
for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`);
}
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
});
if (addResult.status !== 0) {
if (addResult.status !== 0) {
throw new Error(
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
} else {
throw new Error(
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
`Unsupported MCP server type for Gemini: ${(serverConfig as any).type || "unknown"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { show } from "@ark/util";
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
import { log } from "../utils/cli.ts";
@@ -26,7 +26,7 @@ export interface AgentConfig {
apiKey: string;
githubInstallationToken: string;
payload: Payload;
mcpServers: Record<string, McpStdioServerConfig>;
mcpServers: Record<string, McpHttpServerConfig>;
cliPath: string;
}
@@ -34,7 +34,7 @@ export interface AgentConfig {
* Parameters for configuring MCP servers
*/
export interface ConfigureMcpServersParams {
mcpServers: Record<string, McpStdioServerConfig>;
mcpServers: Record<string, McpHttpServerConfig>;
cliPath: string;
}
+76377 -2824
View File
File diff suppressed because one or more lines are too long
+1 -9
View File
@@ -59,7 +59,7 @@ const sharedConfig = {
drop: [],
};
// Build the main entry bundle (without MCP)
// Build the main entry bundle
await build({
...sharedConfig,
entryPoints: ["./entry.ts"],
@@ -67,12 +67,4 @@ await build({
plugins: [stripShebangPlugin],
});
// Build the MCP server bundle
await build({
...sharedConfig,
entryPoints: ["./mcp/server.ts"],
outfile: "./mcp-server",
plugins: [stripShebangPlugin],
});
console.log("✅ Build completed successfully!");
+53 -27
View File
@@ -10,6 +10,7 @@ import type { AgentResult } from "./agents/shared.ts";
import type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts";
import { agentsManifest } from "./external.ts";
import { createMcpConfigs } from "./mcp/config.ts";
import { startMcpHttpServer } from "./mcp/server.ts";
import { modes } from "./modes.ts";
import packageJson from "./package.json" with { type: "json" };
import { fetchRepoSettings } from "./utils/api.ts";
@@ -48,39 +49,29 @@ export interface MainResult {
}
export async function main(inputs: Inputs): Promise<MainResult> {
let githubInstallationToken: string | undefined;
let pollInterval: NodeJS.Timeout | null = null;
let mcpServerClose: (() => Promise<void>) | undefined;
let githubInstallationToken: string | undefined;
try {
// parse payload early to extract agent
const payload = parsePayload(inputs);
// resolve agent before initializing context
githubInstallationToken = await setupGitHubInstallationToken();
const repoContext = parseRepoContext();
const { agentName, agent } = await resolveAgent(
inputs,
payload,
githubInstallationToken,
repoContext
);
const partialCtx = await initializeContext(
inputs,
agentName,
agent,
githubInstallationToken,
repoContext
);
const partialCtx = await initializeContext(inputs, payload);
const ctx = partialCtx as MainContext;
ctx.payload = payload;
githubInstallationToken = ctx.githubInstallationToken;
setupGitAuth(ctx.githubInstallationToken, ctx.repoContext);
setupGitAuth({
githubInstallationToken: ctx.githubInstallationToken,
repoContext: ctx.repoContext,
});
await setupTempDirectory(ctx);
setupMcpLogPolling(ctx);
pollInterval = ctx.pollInterval;
setupGitBranch(ctx.payload);
await startMcpServer(ctx);
mcpServerClose = ctx.mcpServerClose;
setupMcpServers(ctx);
await installAgentCli(ctx);
validateApiKey(ctx);
@@ -99,6 +90,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
if (pollInterval) {
clearInterval(pollInterval);
}
if (mcpServerClose) {
await mcpServerClose();
}
if (githubInstallationToken) {
await revokeInstallationToken(githubInstallationToken);
}
@@ -179,6 +173,8 @@ interface MainContext {
mcpLogPath: string;
pollInterval: NodeJS.Timeout | null;
payload: Payload;
mcpServerUrl: string;
mcpServerClose: () => Promise<void>;
mcpServers: ReturnType<typeof createMcpConfigs>;
cliPath: string;
apiKey: string;
@@ -186,21 +182,33 @@ interface MainContext {
async function initializeContext(
inputs: Inputs,
agentName: AgentNameType,
agent: (typeof agents)[AgentNameType],
githubInstallationToken: string,
repoContext: RepoContext
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
payload: Payload
): Promise<
Omit<MainContext, "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey">
> {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
Inputs.assert(inputs);
setupGitConfig();
const githubInstallationToken = await setupGitHubInstallationToken();
const repoContext = parseRepoContext();
// resolve agent and update payload with resolved agent name
const { agentName, agent } = await resolveAgent(
inputs,
payload,
githubInstallationToken,
repoContext
);
const resolvedPayload = { ...payload, agent: agentName };
return {
inputs,
githubInstallationToken,
repoContext,
agentName,
agent,
payload: resolvedPayload,
sharedTempDir: "",
mcpLogPath: "",
pollInterval: null,
@@ -293,9 +301,27 @@ function parsePayload(inputs: Inputs): Payload {
}
}
function setupMcpServers(ctx: MainContext): void {
async function startMcpServer(ctx: MainContext): Promise<void> {
// Set environment variables for MCP server tools
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
const allModes = [...modes, ...(ctx.payload.modes || [])];
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes, ctx.payload);
process.env.GITHUB_INSTALLATION_TOKEN = ctx.githubInstallationToken;
process.env.GITHUB_REPOSITORY = githubRepository;
process.env.PULLFROG_MODES = JSON.stringify(allModes);
process.env.PULLFROG_PAYLOAD = JSON.stringify(ctx.payload);
// GITHUB_RUN_ID is already set in GitHub Actions, no need to set it here
const { url, close } = await startMcpHttpServer();
ctx.mcpServerUrl = url;
ctx.mcpServerClose = close;
log.info(`🚀 MCP server started at ${url}`);
}
function setupMcpServers(ctx: MainContext): void {
ctx.mcpServers = createMcpConfigs(ctx.mcpServerUrl);
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
}
-107871
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -15,7 +15,7 @@ function buildCommentFooter(payload: Payload): string {
const agentDisplayName = agentInfo?.displayName || "Unknown Agent";
const agentUrl = agentInfo?.url || "https://pullfrog.ai";
// build workflow run URL
// build workflow run URL: https://github.com/{owner}/{repo}/actions/runs/{runId}
const workflowRunUrl = runId
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
+5 -34
View File
@@ -2,47 +2,18 @@
* Simple MCP configuration helper for adding our minimal GitHub comment server
*/
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs";
import type { Payload } from "../external.ts";
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } from "../external.ts";
import type { Mode } from "../modes.ts";
import { parseRepoContext } from "../utils/github.ts";
export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
export function createMcpConfigs(
githubInstallationToken: string,
modes: Mode[],
payload: Payload
): McpConfigs {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
// In production (GitHub Actions), mcp-server is in same directory as entry.js (where this is bundled)
// In development, server.ts is in the same directory as this file (config.ts)
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts");
const env: Record<string, string> = {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
PULLFROG_MODES: JSON.stringify(modes),
PULLFROG_PAYLOAD: JSON.stringify(payload),
PULLFROG_TEMP_DIR: process.env.PULLFROG_TEMP_DIR!,
};
// pass through GITHUB_RUN_ID if available (automatically set in GitHub Actions)
if (process.env.GITHUB_RUN_ID) {
env.GITHUB_RUN_ID = process.env.GITHUB_RUN_ID;
}
export type McpConfigs = Record<McpName, McpHttpServerConfig>;
export function createMcpConfigs(mcpServerUrl: string): McpConfigs {
return {
[ghPullfrogMcpName]: {
command: "node",
args: [serverPath],
env,
type: "http",
url: mcpServerUrl,
},
};
}
+75 -20
View File
@@ -1,5 +1,6 @@
import "./arkConfig.ts";
// this must be imported first
import { createServer } from "node:net";
import { FastMCP } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
@@ -18,25 +19,79 @@ import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComme
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
const server = new FastMCP({
name: ghPullfrogMcpName,
version: "0.0.1",
});
/**
* Find an available port starting from the given port
*/
async function findAvailablePort(startPort: number): Promise<number> {
const checkPort = (port: number): Promise<boolean> => {
return new Promise((resolve) => {
const server = createServer();
server.once("error", () => {
server.close();
resolve(false);
});
server.listen(port, () => {
server.close(() => {
resolve(true);
});
});
});
};
addTools(server, [
SelectModeTool,
CreateCommentTool,
EditCommentTool,
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
GetReviewCommentsTool,
ListPullRequestReviewsTool,
GetCheckSuiteLogsTool,
DebugShellCommandTool,
]);
let port = startPort;
while (port < startPort + 100) {
if (await checkPort(port)) {
return port;
}
port++;
}
throw new Error(`Could not find available port starting from ${startPort}`);
}
server.start();
/**
* Start the MCP HTTP server and return the URL and close function
*/
export async function startMcpHttpServer(): Promise<{ url: string; close: () => Promise<void> }> {
const server = new FastMCP({
name: ghPullfrogMcpName,
version: "0.0.1",
});
addTools(server, [
SelectModeTool,
CreateCommentTool,
EditCommentTool,
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
GetReviewCommentsTool,
ListPullRequestReviewsTool,
GetCheckSuiteLogsTool,
DebugShellCommandTool,
]);
const port = await findAvailablePort(3764);
const host = "127.0.0.1";
const endpoint = "/mcp";
await server.start({
transportType: "httpStream",
httpStream: {
port,
host,
endpoint,
},
});
const url = `http://${host}:${port}${endpoint}`;
return {
url,
close: async () => {
await server.stop();
},
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.112",
"version": "0.0.113",
"type": "module",
"files": [
"index.js",
+5 -2
View File
@@ -72,7 +72,10 @@ export function setupGitConfig(): void {
* Setup git authentication using GitHub installation token
* Always uses the installation token, scoped to the current repo only
*/
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
export function setupGitAuth(ctx: {
githubInstallationToken: string;
repoContext: RepoContext;
}): void {
const repoDir = process.cwd();
log.info("🔐 Setting up git authentication...");
@@ -91,7 +94,7 @@ export function setupGitAuth(githubToken: string, repoContext: RepoContext): voi
// Update remote URL to embed the token
// This is scoped to the repo's .git/config, not the user's global config
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
const remoteUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.repoContext.owner}/${ctx.repoContext.name}.git`;
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
}