Merge branch 'main' of https://github.com/pullfrog/action
This commit is contained in:
+1
-1
@@ -8,5 +8,5 @@ if git diff --cached --name-only | grep -q "^package.json$"; then
|
|||||||
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 mcp-server pnpm-lock.yaml
|
git add entry pnpm-lock.yaml
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
Once configured, you can start triggering agent runs.
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
- **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
|
Create a file at `.github/workflows/triggers.yml`. This workflow listens for GitHub events and calls the `pullfrog.yml` workflow with the event data.
|
||||||
pnpm play # Uses fixtures/play.txt
|
|
||||||
|
```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
|
</details>
|
||||||
- Fast iteration for development
|
|
||||||
|
|||||||
+17
-21
@@ -177,34 +177,30 @@ const messageHandlers: {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure MCP servers for Codex using the CLI.
|
* 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 {
|
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||||
const command = serverConfig.command;
|
if (serverConfig.type === "http") {
|
||||||
const args = serverConfig.args || [];
|
// HTTP-based MCP server - use --url flag
|
||||||
const envVars = serverConfig.env || {};
|
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
|
if (addResult.status !== 0) {
|
||||||
for (const [key, value] of Object.entries(envVars)) {
|
throw new Error(
|
||||||
addArgs.push("--env", `${key}=${value}`);
|
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||||
}
|
);
|
||||||
|
}
|
||||||
addArgs.push("--", command, ...args);
|
log.info(`✓ MCP server '${serverName}' configured`);
|
||||||
|
} else {
|
||||||
log.info(`Adding MCP server '${serverName}'...`);
|
|
||||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
|
||||||
stdio: "pipe",
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addResult.status !== 0) {
|
|
||||||
throw new Error(
|
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
@@ -269,6 +269,22 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
|
|||||||
const cursorConfigDir = join(realHome, ".cursor");
|
const cursorConfigDir = join(realHome, ".cursor");
|
||||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||||
mkdirSync(cursorConfigDir, { recursive: true });
|
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}`);
|
log.info(`MCP config written to ${mcpConfigPath}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -249,31 +249,31 @@ export const gemini = agent({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure MCP servers for Gemini using the CLI.
|
* 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 {
|
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||||
const command = serverConfig.command;
|
if (serverConfig.type === "http") {
|
||||||
const args = serverConfig.args || [];
|
// HTTP-based MCP server - use URL with --transport http flag
|
||||||
const envVars = serverConfig.env || {};
|
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)) {
|
if (addResult.status !== 0) {
|
||||||
addArgs.push("--env", `${key}=${value}`);
|
throw new Error(
|
||||||
}
|
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||||
|
);
|
||||||
log.info(`Adding MCP server '${serverName}'...`);
|
}
|
||||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
log.info(`✓ MCP server '${serverName}' configured`);
|
||||||
stdio: "pipe",
|
} else {
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addResult.status !== 0) {
|
|
||||||
throw new Error(
|
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
@@ -4,7 +4,7 @@ import { mkdtemp } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { pipeline } from "node:stream/promises";
|
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 { show } from "@ark/util";
|
||||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
@@ -26,7 +26,7 @@ export interface AgentConfig {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
githubInstallationToken: string;
|
githubInstallationToken: string;
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
mcpServers: Record<string, McpStdioServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export interface AgentConfig {
|
|||||||
* Parameters for configuring MCP servers
|
* Parameters for configuring MCP servers
|
||||||
*/
|
*/
|
||||||
export interface ConfigureMcpServersParams {
|
export interface ConfigureMcpServersParams {
|
||||||
mcpServers: Record<string, McpStdioServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-9
@@ -59,7 +59,7 @@ const sharedConfig = {
|
|||||||
drop: [],
|
drop: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the main entry bundle (without MCP)
|
// Build the main entry bundle
|
||||||
await build({
|
await build({
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
entryPoints: ["./entry.ts"],
|
entryPoints: ["./entry.ts"],
|
||||||
@@ -67,12 +67,4 @@ await build({
|
|||||||
plugins: [stripShebangPlugin],
|
plugins: [stripShebangPlugin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build the MCP server bundle
|
|
||||||
await build({
|
|
||||||
...sharedConfig,
|
|
||||||
entryPoints: ["./mcp/server.ts"],
|
|
||||||
outfile: "./mcp-server",
|
|
||||||
plugins: [stripShebangPlugin],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ Build completed successfully!");
|
console.log("✅ Build completed successfully!");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { AgentResult } from "./agents/shared.ts";
|
|||||||
import type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts";
|
import type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts";
|
||||||
import { agentsManifest } from "./external.ts";
|
import { agentsManifest } from "./external.ts";
|
||||||
import { createMcpConfigs } from "./mcp/config.ts";
|
import { createMcpConfigs } from "./mcp/config.ts";
|
||||||
|
import { startMcpHttpServer } from "./mcp/server.ts";
|
||||||
import { modes } from "./modes.ts";
|
import { modes } from "./modes.ts";
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import { fetchRepoSettings } from "./utils/api.ts";
|
import { fetchRepoSettings } from "./utils/api.ts";
|
||||||
@@ -48,39 +49,29 @@ export interface MainResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
let githubInstallationToken: string | undefined;
|
|
||||||
let pollInterval: NodeJS.Timeout | null = null;
|
let pollInterval: NodeJS.Timeout | null = null;
|
||||||
|
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||||
|
let githubInstallationToken: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// parse payload early to extract agent
|
// parse payload early to extract agent
|
||||||
const payload = parsePayload(inputs);
|
const payload = parsePayload(inputs);
|
||||||
|
|
||||||
// resolve agent before initializing context
|
const partialCtx = await initializeContext(inputs, payload);
|
||||||
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 ctx = partialCtx as MainContext;
|
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);
|
await setupTempDirectory(ctx);
|
||||||
setupMcpLogPolling(ctx);
|
setupMcpLogPolling(ctx);
|
||||||
pollInterval = ctx.pollInterval;
|
pollInterval = ctx.pollInterval;
|
||||||
|
|
||||||
setupGitBranch(ctx.payload);
|
setupGitBranch(ctx.payload);
|
||||||
|
await startMcpServer(ctx);
|
||||||
|
mcpServerClose = ctx.mcpServerClose;
|
||||||
setupMcpServers(ctx);
|
setupMcpServers(ctx);
|
||||||
await installAgentCli(ctx);
|
await installAgentCli(ctx);
|
||||||
validateApiKey(ctx);
|
validateApiKey(ctx);
|
||||||
@@ -99,6 +90,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
}
|
}
|
||||||
|
if (mcpServerClose) {
|
||||||
|
await mcpServerClose();
|
||||||
|
}
|
||||||
if (githubInstallationToken) {
|
if (githubInstallationToken) {
|
||||||
await revokeInstallationToken(githubInstallationToken);
|
await revokeInstallationToken(githubInstallationToken);
|
||||||
}
|
}
|
||||||
@@ -179,6 +173,8 @@ interface MainContext {
|
|||||||
mcpLogPath: string;
|
mcpLogPath: string;
|
||||||
pollInterval: NodeJS.Timeout | null;
|
pollInterval: NodeJS.Timeout | null;
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
|
mcpServerUrl: string;
|
||||||
|
mcpServerClose: () => Promise<void>;
|
||||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -186,21 +182,33 @@ interface MainContext {
|
|||||||
|
|
||||||
async function initializeContext(
|
async function initializeContext(
|
||||||
inputs: Inputs,
|
inputs: Inputs,
|
||||||
agentName: AgentNameType,
|
payload: Payload
|
||||||
agent: (typeof agents)[AgentNameType],
|
): Promise<
|
||||||
githubInstallationToken: string,
|
Omit<MainContext, "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey">
|
||||||
repoContext: RepoContext
|
> {
|
||||||
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
|
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
Inputs.assert(inputs);
|
Inputs.assert(inputs);
|
||||||
setupGitConfig();
|
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 {
|
return {
|
||||||
inputs,
|
inputs,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
repoContext,
|
repoContext,
|
||||||
agentName,
|
agentName,
|
||||||
agent,
|
agent,
|
||||||
|
payload: resolvedPayload,
|
||||||
sharedTempDir: "",
|
sharedTempDir: "",
|
||||||
mcpLogPath: "",
|
mcpLogPath: "",
|
||||||
pollInterval: null,
|
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 || [])];
|
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)}`);
|
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-107871
File diff suppressed because one or more lines are too long
+1
-1
@@ -15,7 +15,7 @@ function buildCommentFooter(payload: Payload): string {
|
|||||||
const agentDisplayName = agentInfo?.displayName || "Unknown Agent";
|
const agentDisplayName = agentInfo?.displayName || "Unknown Agent";
|
||||||
const agentUrl = agentInfo?.url || "https://pullfrog.ai";
|
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
|
const workflowRunUrl = runId
|
||||||
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
|
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
|
||||||
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
||||||
|
|||||||
+5
-34
@@ -2,47 +2,18 @@
|
|||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { fromHere } from "@ark/fs";
|
|
||||||
import type { Payload } from "../external.ts";
|
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import type { Mode } from "../modes.ts";
|
|
||||||
import { parseRepoContext } from "../utils/github.ts";
|
|
||||||
|
|
||||||
export type McpName = typeof ghPullfrogMcpName;
|
export type McpName = typeof ghPullfrogMcpName;
|
||||||
|
|
||||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
export type McpConfigs = Record<McpName, McpHttpServerConfig>;
|
||||||
|
|
||||||
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 function createMcpConfigs(mcpServerUrl: string): McpConfigs {
|
||||||
return {
|
return {
|
||||||
[ghPullfrogMcpName]: {
|
[ghPullfrogMcpName]: {
|
||||||
command: "node",
|
type: "http",
|
||||||
args: [serverPath],
|
url: mcpServerUrl,
|
||||||
env,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-20
@@ -1,5 +1,6 @@
|
|||||||
import "./arkConfig.ts";
|
import "./arkConfig.ts";
|
||||||
// this must be imported first
|
// this must be imported first
|
||||||
|
import { createServer } from "node:net";
|
||||||
import { FastMCP } from "fastmcp";
|
import { FastMCP } from "fastmcp";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||||
@@ -18,25 +19,79 @@ import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComme
|
|||||||
import { SelectModeTool } from "./selectMode.ts";
|
import { SelectModeTool } from "./selectMode.ts";
|
||||||
import { addTools } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
const server = new FastMCP({
|
/**
|
||||||
name: ghPullfrogMcpName,
|
* Find an available port starting from the given port
|
||||||
version: "0.0.1",
|
*/
|
||||||
});
|
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, [
|
let port = startPort;
|
||||||
SelectModeTool,
|
while (port < startPort + 100) {
|
||||||
CreateCommentTool,
|
if (await checkPort(port)) {
|
||||||
EditCommentTool,
|
return port;
|
||||||
CreateWorkingCommentTool,
|
}
|
||||||
UpdateWorkingCommentTool,
|
port++;
|
||||||
IssueTool,
|
}
|
||||||
PullRequestTool,
|
throw new Error(`Could not find available port starting from ${startPort}`);
|
||||||
ReviewTool,
|
}
|
||||||
PullRequestInfoTool,
|
|
||||||
GetReviewCommentsTool,
|
|
||||||
ListPullRequestReviewsTool,
|
|
||||||
GetCheckSuiteLogsTool,
|
|
||||||
DebugShellCommandTool,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.112",
|
"version": "0.0.113",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+5
-2
@@ -72,7 +72,10 @@ export function setupGitConfig(): void {
|
|||||||
* Setup git authentication using GitHub installation token
|
* Setup git authentication using GitHub installation token
|
||||||
* Always uses the installation token, scoped to the current repo only
|
* 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();
|
const repoDir = process.cwd();
|
||||||
|
|
||||||
log.info("🔐 Setting up git authentication...");
|
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
|
// Update remote URL to embed the token
|
||||||
// This is scoped to the repo's .git/config, not the user's global config
|
// 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 });
|
$("git", ["remote", "set-url", "origin", remoteUrl], { cwd: repoDir });
|
||||||
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
|
log.info("✓ Updated remote URL with authentication token (scoped to repo)");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user