Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16e04e7152 | |||
| 522779ef54 | |||
| d3d2dad025 | |||
| 66bf86f081 | |||
| 608322f026 | |||
| e3a3d416fb | |||
| f0e339f5c2 | |||
| 87d32763e9 | |||
| 671334f37d | |||
| 267ed3686f | |||
| ff1226a824 | |||
| e13c5eed00 | |||
| f2a1c3c1bb | |||
| e672deb934 | |||
| 70b365fca1 |
@@ -0,0 +1,299 @@
|
|||||||
|
# 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.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
GitHub Action for running Claude Code and other agents via Pullfrog.
|
GitHub Action for running Claude Code and other agents via Pullfrog.
|
||||||
|
|
||||||
|
> **📖 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).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+32
-31
@@ -1,8 +1,9 @@
|
|||||||
import { access, constants } from "node:fs/promises";
|
import { access, constants } from "node:fs/promises";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { boxString, tableString } from "../utils";
|
import { createMcpConfig } from "../mcp/config.ts";
|
||||||
import { spawn } from "../utils/subprocess";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import type { Agent, AgentConfig, AgentResult } from "./types";
|
import { boxString, tableString } from "../utils/table.ts";
|
||||||
|
import type { Agent, AgentConfig, AgentResult } from "./types.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code agent implementation
|
* Claude Code agent implementation
|
||||||
@@ -53,13 +54,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
// Use shell execution to properly handle the pipe
|
// Use shell execution to properly handle the pipe
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: [
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
"-c",
|
|
||||||
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
|
|
||||||
],
|
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 120000, // 2 minute timeout
|
timeout: 120000, // 2 minute timeout
|
||||||
onStdout: (chunk) => {
|
onStdout: () => {
|
||||||
// no logs
|
// no logs
|
||||||
// process.stdout.write(chunk)
|
// process.stdout.write(chunk)
|
||||||
},
|
},
|
||||||
@@ -67,9 +65,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||||
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
@@ -98,8 +94,24 @@ export class ClaudeAgent implements Agent {
|
|||||||
"stream-json",
|
"stream-json",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"acceptEdits",
|
"bypassPermissions",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add MCP configuration if GitHub credentials are available
|
||||||
|
if (
|
||||||
|
process.env.GITHUB_INSTALLATION_TOKEN &&
|
||||||
|
process.env.REPO_OWNER &&
|
||||||
|
process.env.REPO_NAME
|
||||||
|
) {
|
||||||
|
const mcpConfig = createMcpConfig(
|
||||||
|
process.env.GITHUB_INSTALLATION_TOKEN,
|
||||||
|
process.env.REPO_OWNER,
|
||||||
|
process.env.REPO_NAME
|
||||||
|
);
|
||||||
|
console.log("📋 MCP Config:", mcpConfig);
|
||||||
|
args.push("--mcp-config", mcpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
ANTHROPIC_API_KEY: this.apiKey,
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
};
|
};
|
||||||
@@ -139,7 +151,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
// throw on non-zero exit code
|
// throw on non-zero exit code
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`,
|
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +170,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
// Log run summary
|
// Log run summary
|
||||||
const duration = Date.now() - this.runStats.startTime;
|
const duration = Date.now() - this.runStats.startTime;
|
||||||
core.info(
|
core.info(
|
||||||
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`,
|
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
||||||
);
|
);
|
||||||
|
|
||||||
core.info("✅ Task complete.");
|
core.info("✅ Task complete.");
|
||||||
@@ -181,8 +193,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
} catch {
|
} catch {
|
||||||
// Group might not have been started, ignore
|
// Group might not have been started, ignore
|
||||||
}
|
}
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
error instanceof Error ? error.message : "Unknown error";
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to execute Claude Code: ${errorMessage}`,
|
error: `Failed to execute Claude Code: ${errorMessage}`,
|
||||||
@@ -233,12 +244,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
[
|
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
||||||
"tools",
|
|
||||||
parsedChunk.tools?.length
|
|
||||||
? `${parsedChunk.tools.length} tools`
|
|
||||||
: "none",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length
|
parsedChunk.mcp_servers?.length
|
||||||
@@ -251,7 +257,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
? `${parsedChunk.slash_commands.length} commands`
|
? `${parsedChunk.slash_commands.length} commands`
|
||||||
: "none",
|
: "none",
|
||||||
],
|
],
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -267,9 +273,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
// Skip empty text content
|
// Skip empty text content
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core.info(
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
boxString(content.text.trim(), { title: "Claude Code" }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
// Track tools used
|
// Track tools used
|
||||||
@@ -379,15 +383,12 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
|
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
[
|
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
||||||
"Cost",
|
|
||||||
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
|
||||||
],
|
|
||||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
["Turns", parsedChunk.num_turns || 1],
|
["Turns", parsedChunk.num_turns || 1],
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
core.error(`❌ Failed: ${parsedChunk.error || "Unknown error"}`);
|
core.error(`❌ Failed: ${parsedChunk.error || "Unknown error"}`);
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./claude";
|
|
||||||
export * from "./types";
|
|
||||||
@@ -19661,10 +19661,10 @@ var require_core = __commonJS({
|
|||||||
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
||||||
}
|
}
|
||||||
exports2.exportVariable = exportVariable;
|
exports2.exportVariable = exportVariable;
|
||||||
function setSecret(secret) {
|
function setSecret2(secret) {
|
||||||
(0, command_1.issueCommand)("add-mask", {}, secret);
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
||||||
}
|
}
|
||||||
exports2.setSecret = setSecret;
|
exports2.setSecret = setSecret2;
|
||||||
function addPath(inputPath) {
|
function addPath(inputPath) {
|
||||||
const filePath = process.env["GITHUB_PATH"] || "";
|
const filePath = process.env["GITHUB_PATH"] || "";
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@@ -25497,51 +25497,32 @@ var require_src = __commonJS({
|
|||||||
var core4 = __toESM(require_core(), 1);
|
var core4 = __toESM(require_core(), 1);
|
||||||
|
|
||||||
// main.ts
|
// main.ts
|
||||||
var core3 = __toESM(require_core(), 1);
|
var core2 = __toESM(require_core(), 1);
|
||||||
|
|
||||||
// agents/claude.ts
|
// agents/claude.ts
|
||||||
var import_promises = require("node:fs/promises");
|
var import_promises = require("node:fs/promises");
|
||||||
var core2 = __toESM(require_core(), 1);
|
|
||||||
|
|
||||||
// utils/github.ts
|
|
||||||
var core = __toESM(require_core(), 1);
|
var core = __toESM(require_core(), 1);
|
||||||
async function setupGitHubInstallationToken() {
|
|
||||||
const inputToken = core.getInput("github_installation_token");
|
// mcp/config.ts
|
||||||
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
var actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
||||||
const existingToken = inputToken || envToken;
|
function createMcpConfig(githubInstallationToken, repoOwner, repoName) {
|
||||||
if (existingToken) {
|
return JSON.stringify(
|
||||||
core.info("Using provided GitHub installation token");
|
{
|
||||||
return existingToken;
|
mcpServers: {
|
||||||
}
|
minimal_github_comment: {
|
||||||
core.info("No cached installation token found, generating OIDC token...");
|
command: "node",
|
||||||
try {
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
env: {
|
||||||
core.info("OIDC token generated successfully");
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
REPO_OWNER: repoOwner,
|
||||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
REPO_NAME: repoName
|
||||||
method: "POST",
|
}
|
||||||
headers: {
|
}
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
if (!tokenResponse.ok) {
|
null,
|
||||||
const errorText = await tokenResponse.text();
|
2
|
||||||
throw new Error(
|
);
|
||||||
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
core.info(
|
|
||||||
`Installation token obtained for ${tokenData.owner}/${tokenData.repository || "all repositories"}`
|
|
||||||
);
|
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
|
||||||
return tokenData.token;
|
|
||||||
} catch (error2) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to setup GitHub installation token: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils/subprocess.ts
|
// utils/subprocess.ts
|
||||||
@@ -25741,30 +25722,25 @@ var ClaudeAgent = class {
|
|||||||
*/
|
*/
|
||||||
async install() {
|
async install() {
|
||||||
if (await this.isClaudeInstalled()) {
|
if (await this.isClaudeInstalled()) {
|
||||||
core2.info("Claude Code is already installed, skipping installation");
|
core.info("Claude Code is already installed, skipping installation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
core2.info("Installing Claude Code...");
|
core.info("Installing Claude Code...");
|
||||||
try {
|
try {
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: [
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
"-c",
|
|
||||||
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"
|
|
||||||
],
|
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 12e4,
|
timeout: 12e4,
|
||||||
// 2 minute timeout
|
// 2 minute timeout
|
||||||
onStdout: (chunk) => {
|
onStdout: () => {
|
||||||
},
|
},
|
||||||
onStderr: (chunk) => process.stderr.write(chunk)
|
onStderr: (chunk) => process.stderr.write(chunk)
|
||||||
});
|
});
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||||
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
core2.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
throw new Error(`Failed to install Claude Code: ${error2}`);
|
throw new Error(`Failed to install Claude Code: ${error2}`);
|
||||||
}
|
}
|
||||||
@@ -25773,7 +25749,7 @@ var ClaudeAgent = class {
|
|||||||
* Execute Claude Code with the given prompt
|
* Execute Claude Code with the given prompt
|
||||||
*/
|
*/
|
||||||
async execute(prompt) {
|
async execute(prompt) {
|
||||||
core2.info("Running Claude Code...");
|
core.info("Running Claude Code...");
|
||||||
try {
|
try {
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
@@ -25783,12 +25759,21 @@ var ClaudeAgent = class {
|
|||||||
"stream-json",
|
"stream-json",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"acceptEdits"
|
"bypassPermissions"
|
||||||
];
|
];
|
||||||
|
if (process.env.GITHUB_INSTALLATION_TOKEN && process.env.REPO_OWNER && process.env.REPO_NAME) {
|
||||||
|
const mcpConfig = createMcpConfig(
|
||||||
|
process.env.GITHUB_INSTALLATION_TOKEN,
|
||||||
|
process.env.REPO_OWNER,
|
||||||
|
process.env.REPO_NAME
|
||||||
|
);
|
||||||
|
console.log("\u{1F4CB} MCP Config:", mcpConfig);
|
||||||
|
args.push("--mcp-config", mcpConfig);
|
||||||
|
}
|
||||||
const env = {
|
const env = {
|
||||||
ANTHROPIC_API_KEY: this.apiKey
|
ANTHROPIC_API_KEY: this.apiKey
|
||||||
};
|
};
|
||||||
core2.startGroup("\u{1F504} Run details");
|
core.startGroup("\u{1F504} Run details");
|
||||||
this.runStats = {
|
this.runStats = {
|
||||||
toolsUsed: 0,
|
toolsUsed: 0,
|
||||||
turns: 0,
|
turns: 0,
|
||||||
@@ -25822,11 +25807,11 @@ Stderr: ${result.stderr}`
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const duration = Date.now() - this.runStats.startTime;
|
const duration = Date.now() - this.runStats.startTime;
|
||||||
core2.info(
|
core.info(
|
||||||
`\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
`\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
||||||
);
|
);
|
||||||
core2.info("\u2705 Task complete.");
|
core.info("\u2705 Task complete.");
|
||||||
core2.endGroup();
|
core.endGroup();
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: finalResult,
|
output: finalResult,
|
||||||
@@ -25839,7 +25824,7 @@ Stderr: ${result.stderr}`
|
|||||||
};
|
};
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
try {
|
try {
|
||||||
core2.endGroup();
|
core.endGroup();
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
|
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
|
||||||
@@ -25857,16 +25842,13 @@ function processJSONChunk(chunk, agent) {
|
|||||||
switch (parsedChunk.type) {
|
switch (parsedChunk.type) {
|
||||||
case "system":
|
case "system":
|
||||||
if (parsedChunk.subtype === "init") {
|
if (parsedChunk.subtype === "init") {
|
||||||
core2.info(`\u{1F680} Starting Claude Code session...`);
|
core.info(`\u{1F680} Starting Claude Code session...`);
|
||||||
core2.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
[
|
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
||||||
"tools",
|
|
||||||
parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length ? `${parsedChunk.mcp_servers.length} servers` : "none"
|
parsedChunk.mcp_servers?.length ? `${parsedChunk.mcp_servers.length} servers` : "none"
|
||||||
@@ -25887,53 +25869,51 @@ function processJSONChunk(chunk, agent) {
|
|||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core2.info(
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
boxString(content.text.trim(), { title: "Claude Code" })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
agent.runStats.toolsUsed++;
|
agent.runStats.toolsUsed++;
|
||||||
}
|
}
|
||||||
const toolName = content.name;
|
const toolName = content.name;
|
||||||
core2.info(`\u2192 ${toolName}`);
|
core.info(`\u2192 ${toolName}`);
|
||||||
if (content.input) {
|
if (content.input) {
|
||||||
const input = content.input;
|
const input = content.input;
|
||||||
if (input.description) {
|
if (input.description) {
|
||||||
core2.info(` \u2514\u2500 ${input.description}`);
|
core.info(` \u2514\u2500 ${input.description}`);
|
||||||
}
|
}
|
||||||
if (input.command) {
|
if (input.command) {
|
||||||
core2.info(` \u2514\u2500 command: ${input.command}`);
|
core.info(` \u2514\u2500 command: ${input.command}`);
|
||||||
}
|
}
|
||||||
if (input.file_path) {
|
if (input.file_path) {
|
||||||
core2.info(` \u2514\u2500 file: ${input.file_path}`);
|
core.info(` \u2514\u2500 file: ${input.file_path}`);
|
||||||
}
|
}
|
||||||
if (input.content) {
|
if (input.content) {
|
||||||
const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content;
|
const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content;
|
||||||
core2.info(` \u2514\u2500 content: ${contentPreview}`);
|
core.info(` \u2514\u2500 content: ${contentPreview}`);
|
||||||
}
|
}
|
||||||
if (input.query) {
|
if (input.query) {
|
||||||
core2.info(` \u2514\u2500 query: ${input.query}`);
|
core.info(` \u2514\u2500 query: ${input.query}`);
|
||||||
}
|
}
|
||||||
if (input.pattern) {
|
if (input.pattern) {
|
||||||
core2.info(` \u2514\u2500 pattern: ${input.pattern}`);
|
core.info(` \u2514\u2500 pattern: ${input.pattern}`);
|
||||||
}
|
}
|
||||||
if (input.url) {
|
if (input.url) {
|
||||||
core2.info(` \u2514\u2500 url: ${input.url}`);
|
core.info(` \u2514\u2500 url: ${input.url}`);
|
||||||
}
|
}
|
||||||
if (input.edits && Array.isArray(input.edits)) {
|
if (input.edits && Array.isArray(input.edits)) {
|
||||||
core2.info(` \u2514\u2500 edits: ${input.edits.length} changes`);
|
core.info(` \u2514\u2500 edits: ${input.edits.length} changes`);
|
||||||
input.edits.forEach((edit, index) => {
|
input.edits.forEach((edit, index) => {
|
||||||
if (edit.file_path) {
|
if (edit.file_path) {
|
||||||
core2.info(` ${index + 1}. ${edit.file_path}`);
|
core.info(` ${index + 1}. ${edit.file_path}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.task) {
|
if (input.task) {
|
||||||
core2.info(` \u2514\u2500 task: ${input.task}`);
|
core.info(` \u2514\u2500 task: ${input.task}`);
|
||||||
}
|
}
|
||||||
if (input.bash_command) {
|
if (input.bash_command) {
|
||||||
core2.info(` \u2514\u2500 bash_command: ${input.bash_command}`);
|
core.info(` \u2514\u2500 bash_command: ${input.bash_command}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25945,7 +25925,7 @@ function processJSONChunk(chunk, agent) {
|
|||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "tool_result") {
|
if (content.type === "tool_result") {
|
||||||
if (content.is_error) {
|
if (content.is_error) {
|
||||||
core2.warning(`\u274C Tool error: ${content.content}`);
|
core.warning(`\u274C Tool error: ${content.content}`);
|
||||||
} else {
|
} else {
|
||||||
const _resultContent = content.content.trim();
|
const _resultContent = content.content.trim();
|
||||||
}
|
}
|
||||||
@@ -25955,12 +25935,9 @@ function processJSONChunk(chunk, agent) {
|
|||||||
break;
|
break;
|
||||||
case "result":
|
case "result":
|
||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
core2.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
[
|
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
||||||
"Cost",
|
|
||||||
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`
|
|
||||||
],
|
|
||||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
@@ -25968,16 +25945,16 @@ function processJSONChunk(chunk, agent) {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
core2.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`);
|
core.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
core2.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`);
|
core.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
core2.debug(`Failed to parse chunk: ${error2}`);
|
core.debug(`Failed to parse chunk: ${error2}`);
|
||||||
core2.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25989,7 +25966,7 @@ async function main(params) {
|
|||||||
process.chdir(cwd);
|
process.chdir(cwd);
|
||||||
}
|
}
|
||||||
Object.assign(process.env, env);
|
Object.assign(process.env, env);
|
||||||
core3.info(`\u2192 Starting agent run with Claude Code`);
|
core2.info(`\u2192 Starting agent run with Claude Code`);
|
||||||
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
||||||
await agent.install();
|
await agent.install();
|
||||||
const result = await agent.execute(inputs.prompt);
|
const result = await agent.execute(inputs.prompt);
|
||||||
@@ -26013,17 +25990,59 @@ async function main(params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// utils/github.ts
|
||||||
|
var core3 = __toESM(require_core(), 1);
|
||||||
|
async function setupGitHubInstallationToken() {
|
||||||
|
const inputToken = core3.getInput("github_installation_token");
|
||||||
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
const existingToken = inputToken || envToken;
|
||||||
|
if (existingToken) {
|
||||||
|
core3.setSecret(existingToken);
|
||||||
|
core3.info("Using provided GitHub installation token");
|
||||||
|
return existingToken;
|
||||||
|
}
|
||||||
|
core3.info("Generating OIDC token...");
|
||||||
|
try {
|
||||||
|
const oidcToken = await core3.getIDToken("pullfrog-api");
|
||||||
|
core3.info("OIDC token generated successfully");
|
||||||
|
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||||
|
core3.info("Exchanging OIDC token for installation token...");
|
||||||
|
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
const errorText = await tokenResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const tokenData = await tokenResponse.json();
|
||||||
|
core3.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
|
core3.setSecret(tokenData.token);
|
||||||
|
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
||||||
|
return tokenData.token;
|
||||||
|
} catch (error2) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to setup GitHub installation token: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// entry.ts
|
// entry.ts
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
const prompt = core4.getInput("prompt", { required: true });
|
const prompt = core4.getInput("prompt", { required: true });
|
||||||
const anthropicApiKey = core4.getInput("anthropic_api_key");
|
const anthropic_api_key = core4.getInput("anthropic_api_key");
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
throw new Error("prompt is required");
|
throw new Error("prompt is required");
|
||||||
}
|
}
|
||||||
const inputs = {
|
const inputs = {
|
||||||
prompt,
|
prompt,
|
||||||
anthropic_api_key: anthropicApiKey
|
anthropic_api_key
|
||||||
};
|
};
|
||||||
const githubToken = core4.getInput("github_token") || process.env.GITHUB_TOKEN;
|
const githubToken = core4.getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
|
|||||||
@@ -6,23 +6,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { main } from "./main";
|
import { type ExecutionInputs, type MainParams, main } from "./main.ts";
|
||||||
import { setupGitHubInstallationToken } from "./utils";
|
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Get inputs from GitHub Actions
|
// Get inputs from GitHub Actions
|
||||||
const prompt = core.getInput("prompt", { required: true });
|
const prompt = core.getInput("prompt", { required: true });
|
||||||
const anthropicApiKey = core.getInput("anthropic_api_key");
|
const anthropic_api_key = core.getInput("anthropic_api_key");
|
||||||
|
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
throw new Error("prompt is required");
|
throw new Error("prompt is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create params object with new structure
|
// Create params object with new structure
|
||||||
const inputs: any = {
|
const inputs: ExecutionInputs = {
|
||||||
prompt,
|
prompt,
|
||||||
anthropic_api_key: anthropicApiKey,
|
anthropic_api_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add optional properties only if they exist
|
// Add optional properties only if they exist
|
||||||
@@ -36,17 +36,15 @@ async function run(): Promise<void> {
|
|||||||
if (githubInstallationToken) {
|
if (githubInstallationToken) {
|
||||||
inputs.github_installation_token = githubInstallationToken;
|
inputs.github_installation_token = githubInstallationToken;
|
||||||
} else {
|
} else {
|
||||||
// Setup GitHub installation token
|
|
||||||
await setupGitHubInstallationToken();
|
await setupGitHubInstallationToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params: MainParams = {
|
||||||
inputs,
|
inputs,
|
||||||
env: {} as Record<string, string>,
|
env: {},
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the main function
|
|
||||||
const result = await main(params);
|
const result = await main(params);
|
||||||
|
|
||||||
// TODO: Set outputs
|
// TODO: Set outputs
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { MainParams } from "../main";
|
import type { MainParams } from "../main.ts";
|
||||||
|
|
||||||
const testParams = {
|
const testParams = {
|
||||||
inputs: {
|
inputs: {
|
||||||
|
|||||||
+3
-1
@@ -1 +1,3 @@
|
|||||||
Print the list of tools available. Then create a new file called test.txt with the content "Hello from Pullfrog!".
|
Use the MCP GitHub comment tool to add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2.
|
||||||
|
|
||||||
|
Do not use the gh cli. If the mcp tool does not work, bail.
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
* This exports the main function for programmatic usage
|
* This exports the main function for programmatic usage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { ClaudeAgent } from "./agents";
|
export { ClaudeAgent } from "./agents/claude.ts";
|
||||||
export type { Agent, AgentConfig, AgentResult } from "./agents/types";
|
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
|
||||||
export {
|
export {
|
||||||
type ExecutionInputs,
|
type ExecutionInputs,
|
||||||
type MainParams,
|
type MainParams,
|
||||||
type MainResult,
|
type MainResult,
|
||||||
main,
|
main,
|
||||||
} from "./main";
|
} from "./main.ts";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { ClaudeAgent } from "./agents";
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
|
|
||||||
// Expected environment variables that should be passed as inputs
|
// Expected environment variables that should be passed as inputs
|
||||||
export const EXPECTED_INPUTS: string[] = [
|
export const EXPECTED_INPUTS: string[] = [
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
"GITHUB_TOKEN",
|
"GITHUB_TOKEN",
|
||||||
"GITHUB_INSTALLATION_TOKEN"
|
"GITHUB_INSTALLATION_TOKEN",
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ExecutionInputs {
|
export interface ExecutionInputs {
|
||||||
@@ -27,7 +27,6 @@ export interface MainResult {
|
|||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function main(params: MainParams): Promise<MainResult> {
|
export async function main(params: MainParams): Promise<MainResult> {
|
||||||
try {
|
try {
|
||||||
// Extract inputs from params
|
// Extract inputs from params
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
|
*/
|
||||||
|
const actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
||||||
|
|
||||||
|
export function createMcpConfig(
|
||||||
|
githubInstallationToken: string,
|
||||||
|
repoOwner: string,
|
||||||
|
repoName: string
|
||||||
|
) {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: {
|
||||||
|
minimal_github_comment: {
|
||||||
|
command: "node",
|
||||||
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
|
env: {
|
||||||
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
REPO_OWNER: repoOwner,
|
||||||
|
REPO_NAME: repoName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Minimal GitHub Issue Comment MCP Server
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Get repository information from environment variables
|
||||||
|
const REPO_OWNER = process.env.REPO_OWNER;
|
||||||
|
const REPO_NAME = process.env.REPO_NAME;
|
||||||
|
|
||||||
|
if (!REPO_OWNER || !REPO_NAME) {
|
||||||
|
console.error("Error: REPO_OWNER and REPO_NAME environment variables are required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "Minimal GitHub Issue Comment Server",
|
||||||
|
version: "0.0.1",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the schema for creating issue comments
|
||||||
|
const Comment = type({
|
||||||
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
|
body: type.string.describe("the comment body content"),
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"create_issue_comment",
|
||||||
|
"Create a comment on a GitHub issue",
|
||||||
|
{
|
||||||
|
issueNumber: z.number().describe("the issue number to comment on"),
|
||||||
|
body: z.string().describe("the comment body content"),
|
||||||
|
},
|
||||||
|
async ({ issueNumber, body }) => {
|
||||||
|
try {
|
||||||
|
Comment.assert({ issueNumber, body });
|
||||||
|
|
||||||
|
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
if (!githubInstallationToken) {
|
||||||
|
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: githubInstallationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await octokit.rest.issues.createComment({
|
||||||
|
owner: REPO_OWNER,
|
||||||
|
repo: REPO_NAME,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
commentId: result.data.id,
|
||||||
|
url: result.data.html_url,
|
||||||
|
body: result.data.body,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error creating comment: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function runServer() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
process.on("exit", () => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
runServer().catch(console.error);
|
||||||
+10
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.8",
|
"version": "0.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -22,21 +22,27 @@
|
|||||||
"build:npm": "zshy",
|
"build:npm": "zshy",
|
||||||
"build:dev": "node esbuild.config.js",
|
"build:dev": "node esbuild.config.js",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"play": "tsx play.ts"
|
"play": "node play.ts",
|
||||||
|
"upDeps": "pnpm up --latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
|
"@octokit/rest": "^22.0.0",
|
||||||
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
|
"arktype": "^2.1.22",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"table": "^6.9.0"
|
"table": "^6.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"commander": "^14.0.0",
|
"arg": "^5.0.2",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.9",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"zshy": "^0.4.1"
|
"zshy": "^0.4.1",
|
||||||
|
"zod": "^3.24.4"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,160 +1,198 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { dirname, extname, join, resolve } from "node:path";
|
import { dirname, extname, join, resolve } from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { Command } from "commander";
|
import arg from "arg";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { main } from "./main";
|
import { main } from "./main.ts";
|
||||||
import { runAct } from "./utils/act";
|
import { runAct } from "./utils/act.ts";
|
||||||
import { setupTestRepo } from "./utils/setup";
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
config();
|
config();
|
||||||
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
async function loadPrompt(filePath: string): Promise<string> {
|
export async function run(
|
||||||
const ext = extname(filePath).toLowerCase();
|
prompt: string,
|
||||||
|
options: { act?: boolean } = {}
|
||||||
// Try to resolve the file path
|
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||||
let resolvedPath: string;
|
|
||||||
|
|
||||||
// First try as fixtures path
|
|
||||||
const fixturesPath = join(__dirname, "fixtures", filePath);
|
|
||||||
if (existsSync(fixturesPath)) {
|
|
||||||
resolvedPath = fixturesPath;
|
|
||||||
} else if (existsSync(filePath)) {
|
|
||||||
resolvedPath = resolve(filePath);
|
|
||||||
} else {
|
|
||||||
throw new Error(`File not found: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ext) {
|
|
||||||
case ".txt": {
|
|
||||||
// Plain text - pass directly as prompt
|
|
||||||
return readFileSync(resolvedPath, "utf8").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
case ".json": {
|
|
||||||
// JSON - stringify and pass as prompt
|
|
||||||
const content = readFileSync(resolvedPath, "utf8");
|
|
||||||
const parsed = JSON.parse(content);
|
|
||||||
return JSON.stringify(parsed, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ".ts": {
|
|
||||||
// TypeScript - dynamic import and stringify default export
|
|
||||||
const fileUrl = pathToFileURL(resolvedPath).href;
|
|
||||||
const module = await import(fileUrl);
|
|
||||||
|
|
||||||
if (!module.default) {
|
|
||||||
throw new Error(`TypeScript file ${filePath} must have a default export`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a string, use it directly
|
|
||||||
if (typeof module.default === "string") {
|
|
||||||
return module.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a MainParams object with a prompt field, extract the prompt
|
|
||||||
if (typeof module.default === "object" && module.default.prompt) {
|
|
||||||
return module.default.prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise stringify it
|
|
||||||
return JSON.stringify(module.default, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported file type: ${ext}. Supported types: .txt, .json, .ts`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPlay(filePath: string, options: { act?: boolean }): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
// Load the prompt from the specified file
|
|
||||||
const prompt = await loadPrompt(filePath);
|
|
||||||
|
|
||||||
if (options.act) {
|
if (options.act) {
|
||||||
// Use Docker/act to run the action
|
// Use Docker/act to run the action
|
||||||
console.log("🐳 Running with Docker/act...");
|
console.log("🐳 Running with Docker/act...");
|
||||||
runAct(prompt);
|
runAct(prompt);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup test repository and run directly
|
||||||
|
const tempDir = join(process.cwd(), ".temp");
|
||||||
|
setupTestRepo({ tempDir, forceClean: true });
|
||||||
|
|
||||||
|
// Change to the temp directory
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(tempDir);
|
||||||
|
|
||||||
|
console.log("🚀 Running action with prompt...");
|
||||||
|
console.log("─".repeat(50));
|
||||||
|
console.log("Prompt:");
|
||||||
|
console.log(prompt);
|
||||||
|
console.log("─".repeat(50));
|
||||||
|
|
||||||
|
// Set environment variables from our .env for the action to use
|
||||||
|
const { EXPECTED_INPUTS } = await import("./main.ts");
|
||||||
|
EXPECTED_INPUTS.forEach((inputName) => {
|
||||||
|
const value = process.env[inputName];
|
||||||
|
if (value) {
|
||||||
|
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run main with the new params structure
|
||||||
|
const inputs: any = {
|
||||||
|
prompt,
|
||||||
|
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add optional properties only if they exist
|
||||||
|
if (process.env.GITHUB_TOKEN) {
|
||||||
|
inputs.github_token = process.env.GITHUB_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.GITHUB_INSTALLATION_TOKEN) {
|
||||||
|
inputs.github_installation_token = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await main({
|
||||||
|
inputs,
|
||||||
|
env: process.env as Record<string, string>,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change back to original directory
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log("✅ Action completed successfully");
|
||||||
|
if (result.output) {
|
||||||
|
console.log("Output:", result.output);
|
||||||
|
}
|
||||||
|
return { success: true, output: result.output || undefined, error: undefined };
|
||||||
} else {
|
} else {
|
||||||
// Setup test repository and run directly
|
console.error("❌ Action failed:", result.error);
|
||||||
const tempDir = join(process.cwd(), ".temp");
|
return { success: false, error: result.error || undefined, output: undefined };
|
||||||
setupTestRepo({ tempDir, forceClean: true });
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
console.error("❌ Error:", errorMessage);
|
||||||
|
return { success: false, error: errorMessage, output: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Change to the temp directory
|
// CLI execution when run directly
|
||||||
process.chdir(tempDir);
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const args = arg({
|
||||||
|
"--help": Boolean,
|
||||||
|
"--act": Boolean,
|
||||||
|
"--raw": String,
|
||||||
|
"-h": "--help",
|
||||||
|
});
|
||||||
|
|
||||||
console.log("🚀 Running test in .temp directory...");
|
if (args["--help"]) {
|
||||||
console.log("─".repeat(50));
|
console.log(`
|
||||||
console.log(`Prompt from ${filePath}:`);
|
Usage: tsx play.ts [file] [options]
|
||||||
console.log(prompt);
|
|
||||||
console.log("─".repeat(50));
|
|
||||||
|
|
||||||
// Set environment variables from our .env for the action to use
|
Test the Pullfrog action with various prompts.
|
||||||
const { EXPECTED_INPUTS } = await import("./main");
|
|
||||||
EXPECTED_INPUTS.forEach((inputName) => {
|
Arguments:
|
||||||
const value = process.env[inputName];
|
file Prompt file to use (.txt, .json, or .ts) [default: fixtures/basic.txt]
|
||||||
if (value) {
|
|
||||||
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
|
Options:
|
||||||
|
--act Use Docker/act to run the action instead of running directly
|
||||||
|
--raw [prompt] Use raw string as prompt instead of loading from file
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
tsx play.ts # Use default fixture
|
||||||
|
tsx play.ts fixtures/basic.txt # Use specific text file
|
||||||
|
tsx play.ts custom.json # Use JSON file
|
||||||
|
tsx play.ts --act fixtures/test.ts # Use TypeScript file with Docker/act
|
||||||
|
tsx play.ts --raw "Hello world" # Use raw string as prompt
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt: string;
|
||||||
|
|
||||||
|
if (args["--raw"]) {
|
||||||
|
// Use raw prompt string
|
||||||
|
prompt = args["--raw"];
|
||||||
|
} else {
|
||||||
|
// Load prompt from file
|
||||||
|
const filePath = args._[0] || "fixtures/basic.txt";
|
||||||
|
const ext = extname(filePath).toLowerCase();
|
||||||
|
let resolvedPath: string;
|
||||||
|
|
||||||
|
// First try as fixtures path
|
||||||
|
const fixturesPath = join(__dirname, "fixtures", filePath);
|
||||||
|
if (existsSync(fixturesPath)) {
|
||||||
|
resolvedPath = fixturesPath;
|
||||||
|
} else if (existsSync(filePath)) {
|
||||||
|
resolvedPath = resolve(filePath);
|
||||||
|
} else {
|
||||||
|
throw new Error(`File not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case ".txt": {
|
||||||
|
// Plain text - pass directly as prompt
|
||||||
|
prompt = readFileSync(resolvedPath, "utf8").trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ".json": {
|
||||||
|
// JSON - stringify and pass as prompt
|
||||||
|
const content = readFileSync(resolvedPath, "utf8");
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
prompt = JSON.stringify(parsed, null, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ".ts": {
|
||||||
|
// TypeScript - dynamic import and stringify default export
|
||||||
|
const fileUrl = pathToFileURL(resolvedPath).href;
|
||||||
|
const module = await import(fileUrl);
|
||||||
|
|
||||||
|
if (!module.default) {
|
||||||
|
throw new Error(`TypeScript file ${filePath} must have a default export`);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Run main with the new params structure
|
// If it's a string, use it directly
|
||||||
const inputs: any = {
|
if (typeof module.default === "string") {
|
||||||
prompt,
|
prompt = module.default;
|
||||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
} else if (typeof module.default === "object" && module.default.prompt) {
|
||||||
};
|
// If it's a MainParams object with a prompt field, extract the prompt
|
||||||
|
prompt = module.default.prompt;
|
||||||
// Add optional properties only if they exist
|
} else {
|
||||||
if (process.env.GITHUB_TOKEN) {
|
// Otherwise stringify it
|
||||||
inputs.github_token = process.env.GITHUB_TOKEN;
|
prompt = JSON.stringify(module.default, null, 2);
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.GITHUB_INSTALLATION_TOKEN) {
|
|
||||||
inputs.github_installation_token = process.env.GITHUB_INSTALLATION_TOKEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await main({
|
|
||||||
inputs,
|
|
||||||
env: process.env as Record<string, string>,
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log("✅ Test completed successfully");
|
|
||||||
if (result.output) {
|
|
||||||
console.log("Output:", result.output);
|
|
||||||
}
|
}
|
||||||
} else {
|
break;
|
||||||
console.error("❌ Test failed:", result.error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported file type: ${ext}. Supported types: .txt, .json, .ts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await run(prompt, { act: args["--act"] || false });
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error:", (error as Error).message);
|
console.error("❌ Error:", (error as Error).message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up CLI
|
|
||||||
const program = new Command();
|
|
||||||
|
|
||||||
program
|
|
||||||
.name("play")
|
|
||||||
.description("Test the Pullfrog action with various prompts")
|
|
||||||
.version("1.0.0")
|
|
||||||
.argument("[file]", "Prompt file to use (.txt, .json, or .ts)", "fixtures/basic.txt")
|
|
||||||
.option("--act", "Use Docker/act to run the action instead of running directly")
|
|
||||||
.action(async (file: string, options: { act?: boolean }) => {
|
|
||||||
await runPlay(file, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse arguments and run
|
|
||||||
program.parseAsync(process.argv).catch((error) => {
|
|
||||||
console.error("Fatal error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
Generated
+972
-167
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub App Installation Token Generator
|
||||||
|
*
|
||||||
|
* Generates a temporary installation token for a GitHub App to access a specific repository.
|
||||||
|
* Uses environment variables for configuration and supports multiple installations.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/generate-installation-token.ts [--repo owner/name] [--update-env]
|
||||||
|
*
|
||||||
|
* Environment variables required:
|
||||||
|
* GITHUB_APP_ID - GitHub App ID
|
||||||
|
* GITHUB_PRIVATE_KEY - GitHub App private key (PEM format)
|
||||||
|
* REPO_OWNER - Target repository owner (default)
|
||||||
|
* REPO_NAME - Target repository name (default)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createSign } from "node:crypto";
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
interface GitHubAppConfig {
|
||||||
|
appId: string;
|
||||||
|
privateKey: string;
|
||||||
|
repoOwner: string;
|
||||||
|
repoName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Installation {
|
||||||
|
id: number;
|
||||||
|
account: {
|
||||||
|
login: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Repository {
|
||||||
|
owner: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallationTokenResponse {
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RepositoriesResponse {
|
||||||
|
repositories: Repository[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitHubAppTokenGenerator {
|
||||||
|
private config: GitHubAppConfig;
|
||||||
|
|
||||||
|
constructor(config: GitHubAppConfig) {
|
||||||
|
// Process private key to handle escaped newlines
|
||||||
|
config.privateKey = config.privateKey.replace(/\\n/g, "\n");
|
||||||
|
this.config = config;
|
||||||
|
this.validateConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateConfig(): void {
|
||||||
|
const { appId, privateKey, repoOwner, repoName } = this.config;
|
||||||
|
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("GITHUB_APP_ID environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error("GITHUB_PRIVATE_KEY environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repoOwner || !repoName) {
|
||||||
|
throw new Error("REPO_OWNER and REPO_NAME environment variables are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privateKey.includes("BEGIN") || !privateKey.includes("END")) {
|
||||||
|
throw new Error("GITHUB_PRIVATE_KEY must be in PEM format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a JWT for GitHub App authentication
|
||||||
|
*/
|
||||||
|
private generateJWT(): string {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const payload = {
|
||||||
|
iat: now - 60, // issued 1 minute ago to account for clock skew
|
||||||
|
exp: now + 5 * 60, // expires in 5 minutes
|
||||||
|
iss: this.config.appId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
alg: "RS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
||||||
|
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
||||||
|
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
||||||
|
|
||||||
|
const signature = createSign("RSA-SHA256")
|
||||||
|
.update(signaturePart)
|
||||||
|
.sign(this.config.privateKey, "base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
|
||||||
|
return `${signaturePart}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private base64UrlEncode(str: string): string {
|
||||||
|
return Buffer.from(str)
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes authenticated requests to GitHub API
|
||||||
|
*/
|
||||||
|
private async githubRequest<T>(
|
||||||
|
path: string,
|
||||||
|
options: {
|
||||||
|
method?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const { method = "GET", headers = {}, body } = options;
|
||||||
|
|
||||||
|
const url = `https://api.github.com${path}`;
|
||||||
|
const requestHeaders = {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: requestHeaders,
|
||||||
|
...(body && { body }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the installation ID for the target repository
|
||||||
|
*/
|
||||||
|
private async findInstallationId(jwt: string): Promise<number> {
|
||||||
|
console.log("🔍 Finding GitHub App installation...");
|
||||||
|
|
||||||
|
const installations = await this.githubRequest<Installation[]>("/app/installations", {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📋 Found ${installations.length} installation(s)`);
|
||||||
|
|
||||||
|
// Check each installation for access to target repository
|
||||||
|
for (const installation of installations) {
|
||||||
|
console.log(`🔎 Checking installation ${installation.id} (${installation.account.login})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a temporary token to check repository access
|
||||||
|
const tempToken = await this.createInstallationToken(jwt, installation.id);
|
||||||
|
const hasAccess = await this.checkRepositoryAccess(tempToken);
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
console.log(
|
||||||
|
`✅ Installation ${installation.id} has access to ${this.config.repoOwner}/${this.config.repoName}`
|
||||||
|
);
|
||||||
|
return installation.id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`❌ Installation ${installation.id} check failed:`,
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`No installation found with access to ${this.config.repoOwner}/${this.config.repoName}. ` +
|
||||||
|
"Ensure the GitHub App is installed on the target repository."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the installation token has access to the target repository
|
||||||
|
*/
|
||||||
|
private async checkRepositoryAccess(token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.githubRequest<RepositoriesResponse>(
|
||||||
|
"/installation/repositories",
|
||||||
|
{
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.repositories.some(
|
||||||
|
(repo) => repo.owner.login === this.config.repoOwner && repo.name === this.config.repoName
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an installation access token
|
||||||
|
*/
|
||||||
|
private async createInstallationToken(jwt: string, installationId: number): Promise<string> {
|
||||||
|
const response = await this.githubRequest<InstallationTokenResponse>(
|
||||||
|
`/app/installations/${installationId}/access_tokens`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new installation token for the configured repository
|
||||||
|
*/
|
||||||
|
async generateToken(): Promise<{
|
||||||
|
token: string;
|
||||||
|
installationId: number;
|
||||||
|
expiresAt: string;
|
||||||
|
}> {
|
||||||
|
console.log(
|
||||||
|
`🚀 Generating installation token for ${this.config.repoOwner}/${this.config.repoName}`
|
||||||
|
);
|
||||||
|
console.log(`📱 App ID: ${this.config.appId}`);
|
||||||
|
|
||||||
|
// Step 1: Generate JWT for app authentication
|
||||||
|
const jwt = this.generateJWT();
|
||||||
|
console.log("🔐 Generated JWT token");
|
||||||
|
|
||||||
|
// Step 2: Find installation with repository access
|
||||||
|
const installationId = await this.findInstallationId(jwt);
|
||||||
|
|
||||||
|
// Step 3: Create installation access token
|
||||||
|
console.log(`🎫 Creating installation token for installation ${installationId}...`);
|
||||||
|
const token = await this.createInstallationToken(jwt, installationId);
|
||||||
|
|
||||||
|
// Calculate expiration (GitHub tokens expire after 1 hour)
|
||||||
|
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
|
console.log("✅ Installation token generated successfully!");
|
||||||
|
console.log(`🎟️ Token: ${token.substring(0, 20)}...`);
|
||||||
|
console.log(`📅 Expires: ${expiresAt}`);
|
||||||
|
console.log(`🏢 Installation ID: ${installationId}`);
|
||||||
|
|
||||||
|
return { token, installationId, expiresAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the .env file with the new installation token
|
||||||
|
*/
|
||||||
|
updateEnvFile(token: string): void {
|
||||||
|
const envPath = join(process.cwd(), ".env");
|
||||||
|
|
||||||
|
try {
|
||||||
|
let envContent = readFileSync(envPath, "utf8");
|
||||||
|
|
||||||
|
// Update or add the installation token
|
||||||
|
const tokenLine = `GITHUB_INSTALLATION_TOKEN=${token}`;
|
||||||
|
const tokenRegex = /^GITHUB_INSTALLATION_TOKEN=.*$/m;
|
||||||
|
|
||||||
|
if (tokenRegex.test(envContent)) {
|
||||||
|
envContent = envContent.replace(tokenRegex, tokenLine);
|
||||||
|
} else {
|
||||||
|
envContent += `\n${tokenLine}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(envPath, envContent);
|
||||||
|
console.log(`📝 Updated ${envPath} with new installation token`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"❌ Failed to update .env file:",
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI interface
|
||||||
|
*/
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const updateEnv = args.includes("--update-env");
|
||||||
|
|
||||||
|
// Parse repository from args if provided
|
||||||
|
const repoArg = args.find((arg) => arg.startsWith("--repo="));
|
||||||
|
let repoOwner = process.env.REPO_OWNER || "pullfrogai";
|
||||||
|
let repoName = process.env.REPO_NAME || "scratch";
|
||||||
|
|
||||||
|
if (repoArg) {
|
||||||
|
const [owner, name] = repoArg.split("=")[1].split("/");
|
||||||
|
if (owner && name) {
|
||||||
|
repoOwner = owner;
|
||||||
|
repoName = name;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid --repo format. Use: --repo=owner/name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: GitHubAppConfig = {
|
||||||
|
appId: process.env.GITHUB_APP_ID!,
|
||||||
|
privateKey: process.env.GITHUB_PRIVATE_KEY!,
|
||||||
|
repoOwner,
|
||||||
|
repoName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = new GitHubAppTokenGenerator(config);
|
||||||
|
const result = await generator.generateToken();
|
||||||
|
|
||||||
|
if (updateEnv) {
|
||||||
|
generator.updateEnvFile(result.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n🎉 Token generation complete!");
|
||||||
|
|
||||||
|
if (!updateEnv) {
|
||||||
|
console.log("\n💡 To automatically update your .env file, run with --update-env flag");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error:", error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GitHubAppTokenGenerator };
|
||||||
+17
-40
@@ -1,45 +1,22 @@
|
|||||||
{
|
{
|
||||||
// Visit https://aka.ms/tsconfig to read more about this file
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// File Layout
|
|
||||||
// "rootDir": "./src",
|
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
"module": "NodeNext",
|
||||||
// Environment Settings
|
"target": "ESNext",
|
||||||
// See also https://aka.ms/tsconfig/module
|
"moduleResolution": "NodeNext",
|
||||||
"module": "esnext",
|
"lib": ["ESNext"],
|
||||||
"moduleResolution": "bundler",
|
"allowImportingTsExtensions": true,
|
||||||
"target": "esnext",
|
"rewriteRelativeImportExtensions": true,
|
||||||
"types": [],
|
"skipLibCheck": true,
|
||||||
// For nodejs:
|
"strict": true,
|
||||||
// "lib": ["esnext"],
|
"noUncheckedSideEffectImports": true,
|
||||||
// "types": ["node"],
|
"declaration": true,
|
||||||
// and npm install -D @types/node
|
"verbatimModuleSyntax": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
// Other Outputs
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"declaration": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"declarationMap": true,
|
"stripInternal": true,
|
||||||
|
"moduleDetection": "force"
|
||||||
// Stricter Typechecking Options
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"exactOptionalPropertyTypes": true,
|
|
||||||
|
|
||||||
// Style Options
|
|
||||||
// "noImplicitReturns": true,
|
|
||||||
// "noImplicitOverride": true,
|
|
||||||
// "noUnusedLocals": true,
|
|
||||||
// "noUnusedParameters": true,
|
|
||||||
// "noFallthroughCasesInSwitch": true,
|
|
||||||
// "noPropertyAccessFromIndexSignature": true,
|
|
||||||
|
|
||||||
// Recommended Options
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noUncheckedSideEffectImports": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"skipLibCheck": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -3,19 +3,19 @@ import { existsSync } from "node:fs";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { buildAction, setupTestRepo } from "./setup";
|
import { buildAction, setupTestRepo } from "./setup.ts";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const tempDir = join(__dirname, "..", ".temp");
|
||||||
|
const actionPath = join(__dirname, "..");
|
||||||
|
const envPath = join(__dirname, "..", "..", ".env");
|
||||||
|
|
||||||
// Environment variables that should be passed as secrets to the workflow
|
// Environment variables that should be passed as secrets to the workflow
|
||||||
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
||||||
|
|
||||||
export function runAct(prompt: string): void {
|
export function runAct(prompt: string): void {
|
||||||
const tempDir = join(__dirname, "..", ".temp");
|
|
||||||
const actionPath = join(__dirname, "..");
|
|
||||||
const envPath = join(__dirname, "..", "..", ".env");
|
|
||||||
|
|
||||||
// Setup test repository
|
// Setup test repository
|
||||||
setupTestRepo({ tempDir });
|
setupTestRepo({ tempDir });
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -1,5 +1,15 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
export interface InstallationToken {
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
|
installation_id: number;
|
||||||
|
repository: string;
|
||||||
|
ref: string;
|
||||||
|
runner_environment: string;
|
||||||
|
owner?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup GitHub installation token for the action
|
* Setup GitHub installation token for the action
|
||||||
*/
|
*/
|
||||||
@@ -7,14 +17,16 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
// Check if we have an installation token from inputs or environment
|
// Check if we have an installation token from inputs or environment
|
||||||
const inputToken = core.getInput("github_installation_token");
|
const inputToken = core.getInput("github_installation_token");
|
||||||
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
|
||||||
const existingToken = inputToken || envToken;
|
const existingToken = inputToken || envToken;
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
|
// Mask the existing token in logs for security
|
||||||
|
core.setSecret(existingToken);
|
||||||
core.info("Using provided GitHub installation token");
|
core.info("Using provided GitHub installation token");
|
||||||
return existingToken;
|
return existingToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("No cached installation token found, generating OIDC token...");
|
core.info("Generating OIDC token...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate OIDC token for our API
|
// Generate OIDC token for our API
|
||||||
@@ -24,6 +36,7 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
// Exchange OIDC token for installation token
|
// Exchange OIDC token for installation token
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||||
|
|
||||||
|
core.info("Exchanging OIDC token for installation token...");
|
||||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -39,10 +52,12 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json();
|
// This type is enforced by us when the response is created
|
||||||
core.info(
|
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
||||||
`Installation token obtained for ${tokenData.owner}/${tokenData.repository || "all repositories"}`
|
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
);
|
|
||||||
|
// Mask the token in logs for security
|
||||||
|
core.setSecret(tokenData.token);
|
||||||
|
|
||||||
// Set the token as an environment variable for this run
|
// Set the token as an environment variable for this run
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from "./files";
|
|
||||||
export * from "./github";
|
|
||||||
export * from "./setup";
|
|
||||||
export * from "./subprocess";
|
|
||||||
export * from "./table";
|
|
||||||
Reference in New Issue
Block a user