Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 008021df1c | |||
| d6bc0fdd64 | |||
| 8fd0328109 | |||
| a1f87ce118 | |||
| 3e7122611c | |||
| 9459803aaa | |||
| f74a75cfac | |||
| 16e04e7152 | |||
| 522779ef54 | |||
| d3d2dad025 | |||
| 66bf86f081 | |||
| 608322f026 | |||
| e3a3d416fb | |||
| f0e339f5c2 | |||
| 87d32763e9 | |||
| 671334f37d | |||
| 267ed3686f | |||
| ff1226a824 | |||
| e13c5eed00 | |||
| f2a1c3c1bb | |||
| e672deb934 | |||
| 70b365fca1 | |||
| 5c8b03a427 | |||
| 92225d30c5 | |||
| 3630ba6618 | |||
| 6a03bb8e1b | |||
| 3139f541e4 | |||
| c5b9c7cfc4 | |||
| 7a14716481 | |||
| 087709f4c7 | |||
| ff81db8bb7 | |||
| bfd948fd3c | |||
| 260b563913 | |||
| 5fef548cee | |||
| 7d633da1be | |||
| ede6cfdfbe | |||
| 7745a0befb |
@@ -0,0 +1,139 @@
|
|||||||
|
name: Publish & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "package.json"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "pnpm"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
|
- name: Get package version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(npm pkg get version | tr -d '"')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Extract major version (e.g., "0" from "0.0.1")
|
||||||
|
MAJOR_VERSION=$(echo $VERSION | cut -d. -f1)
|
||||||
|
echo "major_tag=v$MAJOR_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "📦 Package version: $VERSION"
|
||||||
|
|
||||||
|
- name: Check if tag already exists
|
||||||
|
id: check_tag
|
||||||
|
run: |
|
||||||
|
if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ Tag ${{ steps.version.outputs.tag }} already exists - skipping release"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify built files are up to date
|
||||||
|
if: steps.check_tag.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
# Check if there are any uncommitted changes
|
||||||
|
if [[ -n $(git status --porcelain) ]]; then
|
||||||
|
echo "❌ Error: There are uncommitted changes. Built files should be committed via pre-commit hook."
|
||||||
|
git status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All built files are up to date"
|
||||||
|
|
||||||
|
- name: Build for npm with zshy
|
||||||
|
if: steps.check_tag.outputs.exists == 'false'
|
||||||
|
run: pnpm build:npm
|
||||||
|
|
||||||
|
- name: Create and push tags
|
||||||
|
if: steps.check_tag.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
# Create specific version tag
|
||||||
|
git tag ${{ steps.version.outputs.tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.tag }}
|
||||||
|
|
||||||
|
# Create/update major version tag (moving tag)
|
||||||
|
git tag -f ${{ steps.version.outputs.major_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.major_tag }} --force
|
||||||
|
|
||||||
|
echo "🏷️ Created tags: ${{ steps.version.outputs.tag }} and ${{ steps.version.outputs.major_tag }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.check_tag.outputs.exists == 'false'
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.tag }}
|
||||||
|
release_name: "${{ steps.version.outputs.tag }}"
|
||||||
|
body: |
|
||||||
|
## 📦 @pullfrog/action ${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
### Usage in GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: pullfrog/action@${{ steps.version.outputs.major_tag }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation via npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @pullfrog/action@${{ steps.version.outputs.version }}
|
||||||
|
```
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
# - name: Publish to npm
|
||||||
|
# if: steps.check_tag.outputs.exists == 'false'
|
||||||
|
# run: npm publish --access public
|
||||||
|
# env:
|
||||||
|
# NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## 📊 Publish Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ "${{ steps.check_tag.outputs.exists }}" == "true" ]]; then
|
||||||
|
echo "⚠️ Version ${{ steps.version.outputs.version }} already exists - no action taken" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "✅ Successfully published version ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### 🏷️ Tags Created" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`${{ steps.version.outputs.tag }}\` (specific version)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`${{ steps.version.outputs.major_tag }}\` (major version, auto-updating)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### 📦 Published to" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- GitHub Release: [View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- npm Registry: [@pullfrog/action@${{ steps.version.outputs.version }}](https://www.npmjs.com/package/@pullfrog/action/v/${{ steps.version.outputs.version }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
name: Auto-tag Action Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'package.json'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-tag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
- name: Get package version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(node -p "require('./package.json').version")
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Extract major version (e.g., "0" from "0.0.1")
|
|
||||||
MAJOR_VERSION=$(echo $VERSION | cut -d. -f1)
|
|
||||||
echo "major_tag=v$MAJOR_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Check if tag already exists
|
|
||||||
id: check_tag
|
|
||||||
run: |
|
|
||||||
if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
|
|
||||||
echo "exists=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Tag ${{ steps.version.outputs.tag }} already exists"
|
|
||||||
else
|
|
||||||
echo "exists=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "Tag ${{ steps.version.outputs.tag }} does not exist"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Verify built files are up to date
|
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
|
||||||
run: |
|
|
||||||
# Check if there are any uncommitted changes (built files should already be committed via pre-commit hook)
|
|
||||||
if [[ -n $(git status --porcelain) ]]; then
|
|
||||||
echo "❌ Error: There are uncommitted changes. Built files should be committed via pre-commit hook."
|
|
||||||
git status
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✅ All built files are up to date"
|
|
||||||
|
|
||||||
- name: Create and push tags
|
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
|
||||||
run: |
|
|
||||||
# Create specific version tag
|
|
||||||
git tag ${{ steps.version.outputs.tag }}
|
|
||||||
git push origin ${{ steps.version.outputs.tag }}
|
|
||||||
|
|
||||||
# Create/update major version tag (moving tag)
|
|
||||||
git tag -f ${{ steps.version.outputs.major_tag }}
|
|
||||||
git push origin ${{ steps.version.outputs.major_tag }} --force
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.version.outputs.tag }}
|
|
||||||
release_name: ${{ steps.version.outputs.tag }}
|
|
||||||
body: |
|
|
||||||
Automated release for action version ${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: pullfrog/pullfrog@${{ steps.version.outputs.major_tag }}
|
|
||||||
with:
|
|
||||||
message: "Your message here"
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the specific version:
|
|
||||||
```yaml
|
|
||||||
- uses: pullfrog/pullfrog@${{ steps.version.outputs.tag }}
|
|
||||||
with:
|
|
||||||
message: "Your message here"
|
|
||||||
```
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
+12
-2
@@ -1,4 +1,4 @@
|
|||||||
# macOS settings file
|
# macOS settings file
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Contains all your dependencies
|
# Contains all your dependencies
|
||||||
@@ -34,4 +34,14 @@ yarn-error.log*
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
examples
|
examples
|
||||||
|
|
||||||
|
# Act temporary distribution directory
|
||||||
|
.act-dist/
|
||||||
|
|
||||||
|
# Temporary backup of node_modules
|
||||||
|
.node_modules_backup/
|
||||||
|
|
||||||
|
# Temporary directory for cloned repos
|
||||||
|
.temp/
|
||||||
|
dist
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ echo "🔨 Building action..."
|
|||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Add the built files to the commit
|
# Add the built files to the commit
|
||||||
git add index.cjs dist/
|
git add entry.cjs
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,4 +1,80 @@
|
|||||||
# Pullfrog
|
# Pullfrog Action
|
||||||
|
|
||||||
A simple GitHub Action that prints a customizable message to the console.
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Test with default prompt
|
||||||
|
npm run play # Run locally on your machine
|
||||||
|
npm run play -- --act # Run in Docker (simulates GitHub Actions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with play.ts
|
||||||
|
|
||||||
|
The `play.ts` script provides two ways to test the action:
|
||||||
|
|
||||||
|
### Local Mode (Default)
|
||||||
|
```bash
|
||||||
|
npm run play # Uses fixtures/play.txt
|
||||||
|
npm run play fixtures/complex.txt # Custom prompt file
|
||||||
|
```
|
||||||
|
- Clones the scratch repository to `.temp`
|
||||||
|
- Runs Claude Code directly on your machine
|
||||||
|
- Fast iteration for development
|
||||||
|
|
||||||
|
### Docker Mode (--act flag)
|
||||||
|
```bash
|
||||||
|
npm run play -- --act # Uses fixtures/play.txt
|
||||||
|
npm run play fixtures/simple.txt -- --act # Custom prompt file
|
||||||
|
```
|
||||||
|
- Builds fresh bundles with esbuild
|
||||||
|
- Creates minimal distribution without node_modules
|
||||||
|
- Runs in Docker container via `act`
|
||||||
|
- Simulates GitHub Actions environment
|
||||||
|
|
||||||
|
### Prompt Files
|
||||||
|
|
||||||
|
Supports `.txt`, `.json`, and `.ts` files:
|
||||||
|
```bash
|
||||||
|
npm run play prompt.txt # Plain text prompt
|
||||||
|
npm run play config.json # JSON configuration
|
||||||
|
npm run play dynamic.ts # TypeScript with default export
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build # Production build (bundles & removes node_modules)
|
||||||
|
pnpm build:dev # Development build (keeps node_modules)
|
||||||
|
pnpm dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
The action is bundled into `entry.cjs` with all dependencies included, eliminating runtime dependency on node_modules.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create `.env` in `/action`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Claude API key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **entry.cjs**: Bundled action entry point (self-contained)
|
||||||
|
- **agents/**: Agent implementations (Claude, etc.)
|
||||||
|
- **utils/**: Utilities for subprocess, act, and formatting
|
||||||
|
- **fixtures/**: Test prompt files
|
||||||
|
|
||||||
|
## Why No node_modules?
|
||||||
|
|
||||||
|
pnpm uses symlinks that cause "invalid symlink" errors when `act` copies the action to Docker. Our solution:
|
||||||
|
1. Bundle everything into `entry.cjs`
|
||||||
|
2. Remove node_modules after building
|
||||||
|
3. Create minimal `.act-dist` for Docker testing
|
||||||
+16
-10
@@ -1,20 +1,26 @@
|
|||||||
name: 'Pullfrog Claude Code Action'
|
name: "Pullfrog Claude Code Action"
|
||||||
description: 'Execute Claude Code with a prompt using Anthropic API'
|
description: "Execute Claude Code with a prompt using Anthropic API"
|
||||||
author: 'Pullfrog'
|
author: "Pullfrog"
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
prompt:
|
prompt:
|
||||||
description: 'Prompt to send to Claude Code'
|
description: "Prompt to send to Claude Code"
|
||||||
required: true
|
required: true
|
||||||
default: 'Hello from Claude Code!'
|
default: "Hello from Claude Code!"
|
||||||
anthropic_api_key:
|
anthropic_api_key:
|
||||||
description: 'Anthropic API key for Claude Code authentication'
|
description: "Anthropic API key for Claude Code authentication"
|
||||||
|
required: false
|
||||||
|
github_token:
|
||||||
|
description: "GitHub token for repository access"
|
||||||
|
required: false
|
||||||
|
github_installation_token:
|
||||||
|
description: "GitHub App installation token"
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node20'
|
using: "node20"
|
||||||
main: 'index.cjs'
|
main: "entry.cjs"
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
icon: 'code'
|
icon: "code"
|
||||||
color: 'orange'
|
color: "orange"
|
||||||
|
|||||||
+257
-23
@@ -1,13 +1,20 @@
|
|||||||
|
import { access, constants } from "node:fs/promises";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { executeCommand } from "../utils/exec";
|
import { createMcpConfig } from "../mcp/config.ts";
|
||||||
import { createTempFile } from "../utils/files";
|
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
|
||||||
*/
|
*/
|
||||||
export class ClaudeAgent implements Agent {
|
export class ClaudeAgent implements Agent {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
|
public runStats = {
|
||||||
|
toolsUsed: 0,
|
||||||
|
turns: 0,
|
||||||
|
startTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
@@ -16,13 +23,43 @@ export class ClaudeAgent implements Agent {
|
|||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Claude Code CLI is already installed
|
||||||
|
*/
|
||||||
|
private async isClaudeInstalled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
|
await access(claudePath, constants.F_OK | constants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install Claude Code CLI
|
* Install Claude Code CLI
|
||||||
*/
|
*/
|
||||||
async install(): Promise<void> {
|
async install(): Promise<void> {
|
||||||
|
if (await this.isClaudeInstalled()) {
|
||||||
|
core.info("Claude Code is already installed, skipping installation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
core.info("Installing Claude Code...");
|
core.info("Installing Claude Code...");
|
||||||
try {
|
try {
|
||||||
await executeCommand("curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93");
|
const result = await spawn({
|
||||||
|
cmd: "bash",
|
||||||
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
|
timeout: 120000, // 2 minute timeout
|
||||||
|
onStdout: () => {},
|
||||||
|
onStderr: (chunk) => process.stderr.write(chunk),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to install Claude Code: ${error}`);
|
throw new Error(`Failed to install Claude Code: ${error}`);
|
||||||
@@ -33,41 +70,87 @@ export class ClaudeAgent implements Agent {
|
|||||||
* Execute Claude Code with the given prompt
|
* Execute Claude Code with the given prompt
|
||||||
*/
|
*/
|
||||||
async execute(prompt: string): Promise<AgentResult> {
|
async execute(prompt: string): Promise<AgentResult> {
|
||||||
core.info("Executing Claude Code...");
|
core.info("Running Claude Code...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a temporary file for the prompt
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
const promptFile = createTempFile(prompt, "prompt.txt");
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
|
const args = [
|
||||||
|
"--print",
|
||||||
|
"--output-format",
|
||||||
|
"stream-json",
|
||||||
|
"--verbose",
|
||||||
|
"--permission-mode",
|
||||||
|
"bypassPermissions",
|
||||||
|
];
|
||||||
|
|
||||||
// Execute Claude Code with the prompt
|
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
||||||
const command = `$HOME/.local/bin/claude --dangerously-skip-permissions "${promptFile}"`;
|
throw new Error("GITHUB_INSTALLATION_TOKEN is required for GitHub integration");
|
||||||
core.info(`Executing: ${command}`);
|
}
|
||||||
|
|
||||||
const { stdout, stderr } = await executeCommand(command, {
|
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
||||||
|
console.log("📋 MCP Config:", mcpConfig);
|
||||||
|
args.push("--mcp-config", mcpConfig);
|
||||||
|
|
||||||
|
const env = {
|
||||||
ANTHROPIC_API_KEY: this.apiKey,
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
core.startGroup("🔄 Run details");
|
||||||
|
|
||||||
|
this.runStats = {
|
||||||
|
toolsUsed: 0,
|
||||||
|
turns: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalResult = "";
|
||||||
|
const totalCost = 0;
|
||||||
|
|
||||||
|
const result = await spawn({
|
||||||
|
cmd: claudePath,
|
||||||
|
args,
|
||||||
|
env,
|
||||||
|
input: prompt,
|
||||||
|
timeout: 10 * 60 * 1000, // 10 minutes
|
||||||
|
onStdout: (_chunk) => {
|
||||||
|
processJSONChunk(_chunk, this);
|
||||||
|
},
|
||||||
|
onStderr: (_chunk) => {
|
||||||
|
if (_chunk.trim()) {
|
||||||
|
processJSONChunk(_chunk, this);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stderr) {
|
if (result.exitCode !== 0) {
|
||||||
core.warning(`Claude Code stderr: ${stderr}`);
|
throw new Error(
|
||||||
|
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout) {
|
const duration = Date.now() - this.runStats.startTime;
|
||||||
core.info("Claude Code output:");
|
core.info(
|
||||||
console.log(stdout);
|
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
||||||
}
|
);
|
||||||
|
|
||||||
core.info("Claude Code executed successfully");
|
core.info("✅ Task complete.");
|
||||||
|
core.endGroup(); // End the collapsible log group
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: stdout,
|
output: finalResult,
|
||||||
error: stderr || undefined,
|
|
||||||
metadata: {
|
metadata: {
|
||||||
promptFile,
|
promptLength: prompt.length,
|
||||||
command,
|
exitCode: result.exitCode,
|
||||||
|
durationMs: result.durationMs,
|
||||||
|
totalCost,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
try {
|
||||||
|
core.endGroup();
|
||||||
|
} catch {}
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -76,3 +159,154 @@ export class ClaudeAgent implements Agent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty print a JSON chunk based on its type
|
||||||
|
*/
|
||||||
|
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||||
|
try {
|
||||||
|
console.log(chunk);
|
||||||
|
const parsedChunk = JSON.parse(chunk.trim());
|
||||||
|
|
||||||
|
switch (parsedChunk.type) {
|
||||||
|
case "system":
|
||||||
|
if (parsedChunk.subtype === "init") {
|
||||||
|
core.info(`🚀 Starting Claude Code session...`);
|
||||||
|
core.info(
|
||||||
|
tableString([
|
||||||
|
["model", parsedChunk.model],
|
||||||
|
["cwd", parsedChunk.cwd],
|
||||||
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
|
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
||||||
|
[
|
||||||
|
"mcp_servers",
|
||||||
|
parsedChunk.mcp_servers?.length
|
||||||
|
? `${parsedChunk.mcp_servers.length} servers`
|
||||||
|
: "none",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slash_commands",
|
||||||
|
parsedChunk.slash_commands?.length
|
||||||
|
? `${parsedChunk.slash_commands.length} commands`
|
||||||
|
: "none",
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "assistant":
|
||||||
|
if (parsedChunk.message?.content) {
|
||||||
|
if (agent) {
|
||||||
|
agent.runStats.turns++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const content of parsedChunk.message.content) {
|
||||||
|
if (content.type === "text") {
|
||||||
|
if (content.text.trim()) {
|
||||||
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
|
}
|
||||||
|
} else if (content.type === "tool_use") {
|
||||||
|
if (agent) {
|
||||||
|
agent.runStats.toolsUsed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolName = content.name;
|
||||||
|
|
||||||
|
core.info(`→ ${toolName}`);
|
||||||
|
|
||||||
|
if (content.input) {
|
||||||
|
const input = content.input;
|
||||||
|
|
||||||
|
if (input.description) {
|
||||||
|
core.info(` └─ ${input.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.command) {
|
||||||
|
core.info(` └─ command: ${input.command}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.file_path) {
|
||||||
|
core.info(` └─ file: ${input.file_path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.content) {
|
||||||
|
const contentPreview =
|
||||||
|
input.content.length > 100
|
||||||
|
? `${input.content.substring(0, 100)}...`
|
||||||
|
: input.content;
|
||||||
|
core.info(` └─ content: ${contentPreview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.query) {
|
||||||
|
core.info(` └─ query: ${input.query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.pattern) {
|
||||||
|
core.info(` └─ pattern: ${input.pattern}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.url) {
|
||||||
|
core.info(` └─ url: ${input.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.edits && Array.isArray(input.edits)) {
|
||||||
|
core.info(` └─ edits: ${input.edits.length} changes`);
|
||||||
|
input.edits.forEach((edit: any, index: number) => {
|
||||||
|
if (edit.file_path) {
|
||||||
|
core.info(` ${index + 1}. ${edit.file_path}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.task) {
|
||||||
|
core.info(` └─ task: ${input.task}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.bash_command) {
|
||||||
|
core.info(` └─ bash_command: ${input.bash_command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "user":
|
||||||
|
if (parsedChunk.message?.content) {
|
||||||
|
for (const content of parsedChunk.message.content) {
|
||||||
|
if (content.type === "tool_result") {
|
||||||
|
if (content.is_error) {
|
||||||
|
core.warning(`❌ Tool error: ${content.content}`);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "result":
|
||||||
|
if (parsedChunk.subtype === "success") {
|
||||||
|
core.info(
|
||||||
|
tableString([
|
||||||
|
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
||||||
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
|
["Turns", parsedChunk.num_turns || 1],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
core.error(`❌ Failed: ${parsedChunk.error || "Unknown error"}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to parse chunk: ${error}`);
|
||||||
|
core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { ClaudeAgent } from "./claude";
|
|
||||||
import type { Agent, AgentConfig } from "./types";
|
|
||||||
|
|
||||||
export type AgentType = "claude";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory for creating agent instances
|
|
||||||
*/
|
|
||||||
export function createAgent(type: AgentType, config: AgentConfig): Agent {
|
|
||||||
switch (type) {
|
|
||||||
case "claude":
|
|
||||||
return new ClaudeAgent(config);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported agent type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./claude";
|
|
||||||
export * from "./factory";
|
|
||||||
export * from "./types";
|
|
||||||
Vendored
-17731
File diff suppressed because one or more lines are too long
Regular → Executable
+6593
-329
File diff suppressed because one or more lines are too long
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for GitHub Action
|
||||||
|
* This file is bundled to entry.cjs and called directly by GitHub Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { type ExecutionInputs, type MainParams, main } from "./main.ts";
|
||||||
|
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const prompt = core.getInput("prompt", { required: true });
|
||||||
|
const anthropic_api_key = core.getInput("anthropic_api_key");
|
||||||
|
|
||||||
|
if (!prompt) {
|
||||||
|
throw new Error("prompt is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs: ExecutionInputs = {
|
||||||
|
prompt,
|
||||||
|
anthropic_api_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||||
|
if (githubToken) {
|
||||||
|
inputs.github_token = githubToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubInstallationToken =
|
||||||
|
core.getInput("github_installation_token") || process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
if (githubInstallationToken) {
|
||||||
|
inputs.github_installation_token = githubInstallationToken;
|
||||||
|
} else {
|
||||||
|
await setupGitHubInstallationToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: MainParams = {
|
||||||
|
inputs,
|
||||||
|
env: {},
|
||||||
|
cwd: process.cwd(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await main(params);
|
||||||
|
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Agent execution failed");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
|
core.setFailed(`Action failed: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error("Action failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+9
-7
@@ -1,14 +1,16 @@
|
|||||||
import { build } from 'esbuild';
|
import { build } from "esbuild";
|
||||||
|
|
||||||
|
// Build the GitHub Action bundle only
|
||||||
|
// For npm package builds, use zshy (pnpm build:npm)
|
||||||
await build({
|
await build({
|
||||||
entryPoints: ['./index.ts'],
|
entryPoints: ["./entry.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: './index.cjs',
|
outfile: "./entry.cjs",
|
||||||
format: 'cjs',
|
format: "cjs",
|
||||||
platform: 'node',
|
platform: "node",
|
||||||
target: 'node20',
|
target: "node20",
|
||||||
minify: false,
|
minify: false,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Build completed successfully!');
|
console.log("✅ Build completed successfully!");
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { MainParams } from "../main.ts";
|
||||||
|
|
||||||
|
const testParams = {
|
||||||
|
inputs: {
|
||||||
|
prompt:
|
||||||
|
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
|
||||||
|
anthropic_api_key: "sk-test-key",
|
||||||
|
},
|
||||||
|
env: {},
|
||||||
|
cwd: process.cwd(),
|
||||||
|
} satisfies MainParams;
|
||||||
|
|
||||||
|
export default testParams;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
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.
|
||||||
@@ -1,39 +1,13 @@
|
|||||||
import * as core from "@actions/core";
|
/**
|
||||||
import { createAgent } from "./agents/factory";
|
* Library entry point for npm package
|
||||||
|
* This exports the main function for programmatic usage
|
||||||
|
*/
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
export { ClaudeAgent } from "./agents/claude.ts";
|
||||||
try {
|
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
|
||||||
// Get inputs
|
export {
|
||||||
const prompt = core.getInput("prompt", { required: true });
|
type ExecutionInputs,
|
||||||
const anthropicApiKey = core.getInput("anthropic_api_key", { required: true });
|
type MainParams,
|
||||||
|
type MainResult,
|
||||||
if (!anthropicApiKey) {
|
main,
|
||||||
throw new Error("anthropic_api_key is required");
|
} from "./main.ts";
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`🐸 Pullfrog Claude Code Action starting...`);
|
|
||||||
core.info(`Prompt: ${prompt}`);
|
|
||||||
|
|
||||||
// Create and install the Claude agent
|
|
||||||
const agent = createAgent("claude", { apiKey: anthropicApiKey });
|
|
||||||
await agent.install();
|
|
||||||
|
|
||||||
// Execute the agent with the prompt
|
|
||||||
const result = await agent.execute(prompt);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || "Agent execution failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set outputs
|
|
||||||
core.setOutput("status", "success");
|
|
||||||
core.setOutput("prompt", prompt);
|
|
||||||
core.setOutput("output", result.output || "");
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
||||||
core.setFailed(`Action failed: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute main function
|
|
||||||
main();
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
|
|
||||||
|
export const EXPECTED_INPUTS: string[] = [
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"GITHUB_TOKEN",
|
||||||
|
"GITHUB_INSTALLATION_TOKEN",
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface ExecutionInputs {
|
||||||
|
prompt: string;
|
||||||
|
anthropic_api_key: string;
|
||||||
|
github_token?: string;
|
||||||
|
github_installation_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainParams {
|
||||||
|
inputs: ExecutionInputs;
|
||||||
|
env: Record<string, string>;
|
||||||
|
cwd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainResult {
|
||||||
|
success: boolean;
|
||||||
|
output?: string | undefined;
|
||||||
|
error?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function main(params: MainParams): Promise<MainResult> {
|
||||||
|
try {
|
||||||
|
const { inputs, env, cwd } = params;
|
||||||
|
|
||||||
|
if (cwd !== process.cwd()) {
|
||||||
|
process.chdir(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(process.env, env);
|
||||||
|
|
||||||
|
core.info(`→ Starting agent run with Claude Code`);
|
||||||
|
|
||||||
|
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
||||||
|
await agent.install();
|
||||||
|
|
||||||
|
const result = await agent.execute(inputs.prompt);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: result.error || "Agent execution failed",
|
||||||
|
output: result.output!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: result.output || "",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
const githubRepository = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepository) {
|
||||||
|
throw new Error('GITHUB_REPOSITORY environment variable is required for MCP GitHub integration');
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: {
|
||||||
|
minimal_github_comment: {
|
||||||
|
command: "node",
|
||||||
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
|
env: {
|
||||||
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/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";
|
||||||
|
import { resolveRepoContext } from "../utils/repo-context.ts";
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve repository context from environment
|
||||||
|
const repoContext = resolveRepoContext();
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: githubInstallationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await octokit.rest.issues.createComment({
|
||||||
|
owner: repoContext.owner,
|
||||||
|
repo: repoContext.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);
|
||||||
Generated
-601
@@ -1,601 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "action",
|
|
||||||
"version": "0.0.2",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "action",
|
|
||||||
"version": "0.0.2",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/core": "^1.10.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.10.0",
|
|
||||||
"esbuild": "^0.25.9",
|
|
||||||
"typescript": "^5.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@actions/core": {
|
|
||||||
"version": "1.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
|
||||||
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/exec": "^1.1.1",
|
|
||||||
"@actions/http-client": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@actions/exec": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/io": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@actions/http-client": {
|
|
||||||
"version": "2.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
|
|
||||||
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tunnel": "^0.0.6",
|
|
||||||
"undici": "^5.25.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@actions/io": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fastify/busboy": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "20.19.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
|
|
||||||
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~6.21.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.25.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
|
||||||
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.25.9",
|
|
||||||
"@esbuild/android-arm": "0.25.9",
|
|
||||||
"@esbuild/android-arm64": "0.25.9",
|
|
||||||
"@esbuild/android-x64": "0.25.9",
|
|
||||||
"@esbuild/darwin-arm64": "0.25.9",
|
|
||||||
"@esbuild/darwin-x64": "0.25.9",
|
|
||||||
"@esbuild/freebsd-arm64": "0.25.9",
|
|
||||||
"@esbuild/freebsd-x64": "0.25.9",
|
|
||||||
"@esbuild/linux-arm": "0.25.9",
|
|
||||||
"@esbuild/linux-arm64": "0.25.9",
|
|
||||||
"@esbuild/linux-ia32": "0.25.9",
|
|
||||||
"@esbuild/linux-loong64": "0.25.9",
|
|
||||||
"@esbuild/linux-mips64el": "0.25.9",
|
|
||||||
"@esbuild/linux-ppc64": "0.25.9",
|
|
||||||
"@esbuild/linux-riscv64": "0.25.9",
|
|
||||||
"@esbuild/linux-s390x": "0.25.9",
|
|
||||||
"@esbuild/linux-x64": "0.25.9",
|
|
||||||
"@esbuild/netbsd-arm64": "0.25.9",
|
|
||||||
"@esbuild/netbsd-x64": "0.25.9",
|
|
||||||
"@esbuild/openbsd-arm64": "0.25.9",
|
|
||||||
"@esbuild/openbsd-x64": "0.25.9",
|
|
||||||
"@esbuild/openharmony-arm64": "0.25.9",
|
|
||||||
"@esbuild/sunos-x64": "0.25.9",
|
|
||||||
"@esbuild/win32-arm64": "0.25.9",
|
|
||||||
"@esbuild/win32-ia32": "0.25.9",
|
|
||||||
"@esbuild/win32-x64": "0.25.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tunnel": {
|
|
||||||
"version": "0.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
|
||||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici": {
|
|
||||||
"version": "5.29.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
|
||||||
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@fastify/busboy": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "6.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+48
-12
@@ -1,36 +1,72 @@
|
|||||||
{
|
{
|
||||||
"name": "action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.5",
|
"version": "0.0.18",
|
||||||
"main": "index.js",
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"index.cjs",
|
||||||
|
"index.d.ts",
|
||||||
|
"index.d.cts",
|
||||||
|
"agents",
|
||||||
|
"utils",
|
||||||
|
"main.js",
|
||||||
|
"main.d.ts"
|
||||||
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"example": "examples"
|
"example": "examples"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "node esbuild.config.js",
|
"build": "node esbuild.config.js",
|
||||||
"dev": "node esbuild.config.js --watch",
|
"build:npm": "zshy",
|
||||||
"prepare": "husky"
|
"build:dev": "node esbuild.config.js",
|
||||||
|
"prepare": "husky",
|
||||||
|
"play": "node play.ts",
|
||||||
|
"upDeps": "pnpm up --latest",
|
||||||
|
"createLockfile": "pnpm --ignore-workspace install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.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",
|
||||||
|
"execa": "^9.6.0",
|
||||||
|
"table": "^6.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.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",
|
||||||
|
"zod": "^3.24.4"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/pullfrog/pullfrog.git"
|
"url": "git+https://github.com/pullfrog/action.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/pullfrog/pullfrog/issues"
|
"url": "https://github.com/pullfrog/action/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/pullfrog/pullfrog#readme"
|
"homepage": "https://github.com/pullfrog/action#readme",
|
||||||
|
"zshy": {
|
||||||
|
"exports": "./index.ts"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { dirname, extname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
import arg from "arg";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import { main } from "./main.ts";
|
||||||
|
import { runAct } from "./utils/act.ts";
|
||||||
|
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export async function run(
|
||||||
|
prompt: string,
|
||||||
|
options: { act?: boolean } = {}
|
||||||
|
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||||
|
try {
|
||||||
|
if (options.act) {
|
||||||
|
console.log("🐳 Running with Docker/act...");
|
||||||
|
runAct(prompt);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDir = join(process.cwd(), ".temp");
|
||||||
|
setupTestRepo({ tempDir, forceClean: true });
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
const { EXPECTED_INPUTS } = await import("./main.ts");
|
||||||
|
EXPECTED_INPUTS.forEach((inputName) => {
|
||||||
|
const value = process.env[inputName];
|
||||||
|
if (value) {
|
||||||
|
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs: any = {
|
||||||
|
prompt,
|
||||||
|
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.GITHUB_TOKEN) {
|
||||||
|
inputs.github_token = process.env.GITHUB_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔑 Setting up GitHub installation token...");
|
||||||
|
const installationToken = await setupGitHubInstallationToken();
|
||||||
|
inputs.github_installation_token = installationToken;
|
||||||
|
console.log("✅ GitHub installation token setup successfully");
|
||||||
|
|
||||||
|
const envWithToken = {
|
||||||
|
...process.env,
|
||||||
|
GITHUB_INSTALLATION_TOKEN: installationToken,
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
|
const result = await main({
|
||||||
|
inputs,
|
||||||
|
env: envWithToken,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
console.error("❌ Action failed:", result.error);
|
||||||
|
return { success: false, error: result.error || undefined, output: undefined };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
console.error("❌ Error:", errorMessage);
|
||||||
|
return { success: false, error: errorMessage, output: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const args = arg({
|
||||||
|
"--help": Boolean,
|
||||||
|
"--act": Boolean,
|
||||||
|
"--raw": String,
|
||||||
|
"-h": "--help",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args["--help"]) {
|
||||||
|
console.log(`
|
||||||
|
Usage: tsx play.ts [file] [options]
|
||||||
|
|
||||||
|
Test the Pullfrog action with various prompts.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
file Prompt file to use (.txt, .json, or .ts) [default: fixtures/basic.txt]
|
||||||
|
|
||||||
|
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"]) {
|
||||||
|
prompt = args["--raw"];
|
||||||
|
} else {
|
||||||
|
const filePath = args._[0] || "fixtures/basic.txt";
|
||||||
|
const ext = extname(filePath).toLowerCase();
|
||||||
|
let resolvedPath: string;
|
||||||
|
|
||||||
|
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": {
|
||||||
|
prompt = readFileSync(resolvedPath, "utf8").trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ".json": {
|
||||||
|
const content = readFileSync(resolvedPath, "utf8");
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
prompt = JSON.stringify(parsed, null, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ".ts": {
|
||||||
|
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 (typeof module.default === "string") {
|
||||||
|
prompt = module.default;
|
||||||
|
} else if (typeof module.default === "object" && module.default.prompt) {
|
||||||
|
prompt = module.default.prompt;
|
||||||
|
} else {
|
||||||
|
prompt = JSON.stringify(module.default, null, 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.error("❌ Error:", (error as Error).message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1426
-117
File diff suppressed because it is too large
Load Diff
+19
-14
@@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"outDir": "./dist",
|
||||||
"module": "commonjs",
|
"module": "NodeNext",
|
||||||
"lib": ["ES2020"],
|
"target": "ESNext",
|
||||||
"outDir": "./",
|
"moduleResolution": "NodeNext",
|
||||||
"rootDir": "./",
|
"lib": ["ESNext"],
|
||||||
"strict": true,
|
"allowImportingTsExtensions": true,
|
||||||
"esModuleInterop": true,
|
"rewriteRelativeImportExtensions": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"strict": true,
|
||||||
"declaration": false,
|
"noUncheckedSideEffectImports": true,
|
||||||
"sourceMap": false
|
"declaration": true,
|
||||||
},
|
"verbatimModuleSyntax": true,
|
||||||
"include": ["*.ts"],
|
"esModuleInterop": true,
|
||||||
"exclude": ["node_modules", "**/*.test.ts"]
|
"resolveJsonModule": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"stripInternal": true,
|
||||||
|
"moduleDetection": "force"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import { buildAction, setupTestRepo } from "./setup.ts";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const tempDir = join(__dirname, "..", ".temp");
|
||||||
|
const actionPath = join(__dirname, "..");
|
||||||
|
const envPath = join(__dirname, "..", "..", ".env");
|
||||||
|
|
||||||
|
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
||||||
|
|
||||||
|
export function runAct(prompt: string): void {
|
||||||
|
setupTestRepo({ tempDir });
|
||||||
|
|
||||||
|
config({ path: envPath });
|
||||||
|
|
||||||
|
buildAction(actionPath);
|
||||||
|
|
||||||
|
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||||
|
|
||||||
|
const distPath = join(actionPath, ".act-dist");
|
||||||
|
console.log("📦 Creating minimal distribution for act...");
|
||||||
|
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
|
||||||
|
|
||||||
|
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
|
||||||
|
const src = join(actionPath, file);
|
||||||
|
if (existsSync(src)) {
|
||||||
|
execSync(`cp "${src}" "${distPath}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||||
|
|
||||||
|
const actCommandParts = [
|
||||||
|
"act",
|
||||||
|
"workflow_dispatch",
|
||||||
|
"-W",
|
||||||
|
workflowPath,
|
||||||
|
"--input",
|
||||||
|
`prompt='${escapedPrompt}'`,
|
||||||
|
"--local-repository",
|
||||||
|
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
|
||||||
|
];
|
||||||
|
|
||||||
|
ENV_VARS.forEach((key) => {
|
||||||
|
if (process.env[key]) {
|
||||||
|
actCommandParts.push("-s", key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const actCommand = actCommandParts.join(" ");
|
||||||
|
|
||||||
|
console.log("🚀 Running act with prompt:");
|
||||||
|
console.log("─".repeat(50));
|
||||||
|
console.log(prompt);
|
||||||
|
console.log("─".repeat(50));
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
execSync(actCommand, {
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: join(__dirname, "..", ".."),
|
||||||
|
});
|
||||||
|
execSync(`rm -rf "${distPath}"`);
|
||||||
|
} catch (error) {
|
||||||
|
execSync(`rm -rf "${distPath}"`);
|
||||||
|
console.error("❌ Act execution failed:", (error as Error).message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { exec } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
|
|
||||||
export const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a shell command with optional environment variables
|
|
||||||
*/
|
|
||||||
export async function executeCommand(
|
|
||||||
command: string,
|
|
||||||
env?: Record<string, string>
|
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
|
||||||
const execEnv = env ? { ...process.env, ...env } : process.env;
|
|
||||||
return execAsync(command, { env: execEnv });
|
|
||||||
}
|
|
||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
import { createSign } from "node:crypto";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { resolveRepoContext } from "./repo-context.ts";
|
||||||
|
|
||||||
|
export interface InstallationToken {
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
|
installation_id: number;
|
||||||
|
repository: string;
|
||||||
|
ref: string;
|
||||||
|
runner_environment: string;
|
||||||
|
owner?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExistingToken(): string | null {
|
||||||
|
const inputToken = core.getInput("github_installation_token");
|
||||||
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
return inputToken || envToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitHubActionsEnvironment(): boolean {
|
||||||
|
return Boolean(process.env.GITHUB_ACTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireTokenViaOIDC(): Promise<string> {
|
||||||
|
core.info("Generating OIDC token...");
|
||||||
|
|
||||||
|
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||||
|
core.info("OIDC token generated successfully");
|
||||||
|
|
||||||
|
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`, {
|
||||||
|
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()) as InstallationToken;
|
||||||
|
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
|
|
||||||
|
return tokenData.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64UrlEncode = (str: string): string => {
|
||||||
|
return Buffer.from(str)
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateJWT = (appId: string, privateKey: string): string => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const payload = {
|
||||||
|
iat: now - 60,
|
||||||
|
exp: now + 5 * 60,
|
||||||
|
iss: appId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
alg: "RS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
||||||
|
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
||||||
|
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
||||||
|
|
||||||
|
const signature = createSign("RSA-SHA256")
|
||||||
|
.update(signaturePart)
|
||||||
|
.sign(privateKey, "base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
|
||||||
|
return `${signaturePart}.${signature}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const githubRequest = async <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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkRepositoryAccess = async (
|
||||||
|
token: string,
|
||||||
|
repoOwner: string,
|
||||||
|
repoName: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await githubRequest<RepositoriesResponse>("/installation/repositories", {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.repositories.some(
|
||||||
|
(repo) => repo.owner.login === repoOwner && repo.name === repoName
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInstallationToken = async (jwt: string, installationId: number): Promise<string> => {
|
||||||
|
const response = await githubRequest<InstallationTokenResponse>(
|
||||||
|
`/app/installations/${installationId}/access_tokens`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findInstallationId = async (
|
||||||
|
jwt: string,
|
||||||
|
repoOwner: string,
|
||||||
|
repoName: string
|
||||||
|
): Promise<number> => {
|
||||||
|
const installations = await githubRequest<Installation[]>("/app/installations", {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const installation of installations) {
|
||||||
|
try {
|
||||||
|
const tempToken = await createInstallationToken(jwt, installation.id);
|
||||||
|
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
return installation.id;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`No installation found with access to ${repoOwner}/${repoName}. ` +
|
||||||
|
"Ensure the GitHub App is installed on the target repository."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function acquireTokenViaGitHubApp(): Promise<string> {
|
||||||
|
const repoContext = resolveRepoContext();
|
||||||
|
|
||||||
|
const config: GitHubAppConfig = {
|
||||||
|
appId: process.env.GITHUB_APP_ID!,
|
||||||
|
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!,
|
||||||
|
repoOwner: repoContext.owner,
|
||||||
|
repoName: repoContext.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwt = generateJWT(config.appId, config.privateKey);
|
||||||
|
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
|
||||||
|
const token = await createInstallationToken(jwt, installationId);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireNewToken(): Promise<string> {
|
||||||
|
if (isGitHubActionsEnvironment()) {
|
||||||
|
return await acquireTokenViaOIDC();
|
||||||
|
} else {
|
||||||
|
return await acquireTokenViaGitHubApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup GitHub installation token for the action
|
||||||
|
*/
|
||||||
|
export async function setupGitHubInstallationToken(): Promise<string> {
|
||||||
|
const existingToken = checkExistingToken();
|
||||||
|
if (existingToken) {
|
||||||
|
core.setSecret(existingToken);
|
||||||
|
core.info("Using provided GitHub installation token");
|
||||||
|
return existingToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await acquireNewToken();
|
||||||
|
|
||||||
|
core.setSecret(token);
|
||||||
|
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export interface RepoContext {
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve repository context from GITHUB_REPOSITORY environment variable.
|
||||||
|
* Throws if not available.
|
||||||
|
*/
|
||||||
|
export function resolveRepoContext(): RepoContext {
|
||||||
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepo) {
|
||||||
|
throw new Error('GITHUB_REPOSITORY environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, name] = githubRepo.split('/');
|
||||||
|
if (!owner || !name) {
|
||||||
|
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { existsSync, rmSync } from "node:fs";
|
||||||
|
|
||||||
|
export interface SetupOptions {
|
||||||
|
tempDir: string;
|
||||||
|
repoUrl?: string;
|
||||||
|
forceClean?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the test repository for running actions
|
||||||
|
*/
|
||||||
|
export function setupTestRepo(options: SetupOptions): void {
|
||||||
|
const {
|
||||||
|
tempDir,
|
||||||
|
repoUrl = "git@github.com:pullfrogai/scratch.git",
|
||||||
|
forceClean = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (existsSync(tempDir)) {
|
||||||
|
if (forceClean) {
|
||||||
|
console.log("🗑️ Removing existing .temp directory...");
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
||||||
|
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
||||||
|
} else {
|
||||||
|
console.log("📦 Resetting existing .temp repository...");
|
||||||
|
execSync("git reset --hard HEAD && git clean -fd", {
|
||||||
|
cwd: tempDir,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
||||||
|
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the action bundles
|
||||||
|
*/
|
||||||
|
export function buildAction(actionPath: string): void {
|
||||||
|
console.log("🔨 Building fresh bundles with esbuild...");
|
||||||
|
execSync("node esbuild.config.js", {
|
||||||
|
cwd: actionPath,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { spawn as nodeSpawn } from "node:child_process";
|
||||||
|
|
||||||
|
export interface SpawnOptions {
|
||||||
|
cmd: string;
|
||||||
|
args: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
input?: string;
|
||||||
|
timeout?: number;
|
||||||
|
onStdout?: (chunk: string) => void;
|
||||||
|
onStderr?: (chunk: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpawnResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
exitCode: number;
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a subprocess with streaming callbacks and buffered results
|
||||||
|
*/
|
||||||
|
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||||
|
const { cmd, args, env, input, timeout, onStdout, onStderr } = options;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
let stdoutBuffer = "";
|
||||||
|
let stderrBuffer = "";
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = nodeSpawn(cmd, args, {
|
||||||
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
let isTimedOut = false;
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
isTimedOut = true;
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!child.killed) {
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.stdout) {
|
||||||
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
|
const chunk = data.toString();
|
||||||
|
stdoutBuffer += chunk;
|
||||||
|
onStdout?.(chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.stderr) {
|
||||||
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
|
const chunk = data.toString();
|
||||||
|
stderrBuffer += chunk;
|
||||||
|
onStderr?.(chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
child.on("close", (exitCode) => {
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTimedOut) {
|
||||||
|
reject(new Error(`Process timed out after ${timeout}ms`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
stdout: stdoutBuffer,
|
||||||
|
stderr: stderrBuffer,
|
||||||
|
exitCode: exitCode || 0,
|
||||||
|
durationMs,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (_error) => {
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
stdout: stdoutBuffer,
|
||||||
|
stderr: stderrBuffer,
|
||||||
|
exitCode: 1,
|
||||||
|
durationMs,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input && child.stdin) {
|
||||||
|
child.stdin.write(input);
|
||||||
|
child.stdin.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
import { table } from "table";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a formatted table with consistent styling
|
||||||
|
* @param rows - Array of string arrays representing table rows
|
||||||
|
* @param options - Optional table configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function tableString(
|
||||||
|
rows: string[][],
|
||||||
|
options?: {
|
||||||
|
title?: string;
|
||||||
|
indent?: string;
|
||||||
|
drawHorizontalLine?: (lineIndex: number, rowCount: number) => boolean;
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
indent = "",
|
||||||
|
drawHorizontalLine = (lineIndex: number, rowCount: number) => {
|
||||||
|
return lineIndex === 0 || (options?.title && lineIndex === 1) || lineIndex === rowCount;
|
||||||
|
},
|
||||||
|
} = options || {};
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
rows.unshift([title]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableOutput = table(rows, {
|
||||||
|
drawHorizontalLine,
|
||||||
|
border: {
|
||||||
|
topBody: `─`,
|
||||||
|
topJoin: `┬`,
|
||||||
|
topLeft: `┌`,
|
||||||
|
topRight: `┐`,
|
||||||
|
|
||||||
|
bottomBody: `─`,
|
||||||
|
bottomJoin: `┴`,
|
||||||
|
bottomLeft: `└`,
|
||||||
|
bottomRight: `┘`,
|
||||||
|
|
||||||
|
bodyLeft: `│`,
|
||||||
|
bodyRight: `│`,
|
||||||
|
bodyJoin: `│`,
|
||||||
|
|
||||||
|
joinBody: `─`,
|
||||||
|
joinLeft: `├`,
|
||||||
|
joinRight: `┤`,
|
||||||
|
joinJoin: `┼`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const indentedOutput = tableOutput.split("\n").join(`\n${indent}`).trim();
|
||||||
|
|
||||||
|
return `${indent}${indentedOutput}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a multi-line string in a formatted box with text wrapping
|
||||||
|
* @param text - The text to display
|
||||||
|
* @param options - Optional configuration for the box
|
||||||
|
*/
|
||||||
|
export function boxString(
|
||||||
|
text: string,
|
||||||
|
options?: {
|
||||||
|
title?: string;
|
||||||
|
maxWidth?: number;
|
||||||
|
indent?: string;
|
||||||
|
padding?: number;
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
const { title, maxWidth = 80, indent = "", padding = 1 } = options || {};
|
||||||
|
|
||||||
|
// Clean up the text and split into lines
|
||||||
|
const lines = text.trim().split("\n");
|
||||||
|
|
||||||
|
// Word wrap each line to fit within maxWidth
|
||||||
|
const wrappedLines: string[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.length <= maxWidth - padding * 2) {
|
||||||
|
wrappedLines.push(line);
|
||||||
|
} else {
|
||||||
|
// Word wrap the line
|
||||||
|
const words = line.split(" ");
|
||||||
|
let currentLine = "";
|
||||||
|
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||||
|
if (testLine.length <= maxWidth - padding * 2) {
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
currentLine = word;
|
||||||
|
} else {
|
||||||
|
// Word is too long, break it
|
||||||
|
wrappedLines.push(word.substring(0, maxWidth - padding * 2));
|
||||||
|
currentLine = word.substring(maxWidth - padding * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the maximum line length for box sizing
|
||||||
|
const maxLineLength = Math.max(...wrappedLines.map((line) => line.length));
|
||||||
|
const boxWidth = maxLineLength + padding * 2;
|
||||||
|
|
||||||
|
// Create the box
|
||||||
|
const topBorder = "┌" + "─".repeat(boxWidth) + "┐";
|
||||||
|
const bottomBorder = "└" + "─".repeat(boxWidth) + "┘";
|
||||||
|
const sideBorder = "│";
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
// Add title if provided
|
||||||
|
if (title) {
|
||||||
|
const titleLine = ` ${title} `;
|
||||||
|
const titlePadding = Math.max(0, boxWidth - titleLine.length);
|
||||||
|
result += `${indent}┌${titleLine}${"─".repeat(titlePadding)}┐\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add top border (or title border)
|
||||||
|
if (!title) {
|
||||||
|
result += `${indent}${topBorder}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content lines
|
||||||
|
for (const line of wrappedLines) {
|
||||||
|
const paddedLine = line.padEnd(maxLineLength);
|
||||||
|
result += `${indent}${sideBorder}${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}${sideBorder}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bottom border
|
||||||
|
result += `${indent}${bottomBorder}`;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user