mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f6e3225b0 | ||
|
|
6672e9b357 | ||
|
|
950bdc01df | ||
|
|
15dd796e97 | ||
|
|
fd012347a2 | ||
|
|
5bdc533a52 | ||
|
|
d45539c118 | ||
|
|
daac7e353f | ||
|
|
bdfdd1f788 | ||
|
|
ec0e9b4f87 | ||
|
|
af32fd318a | ||
|
|
e07ea013bd |
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -30,4 +30,4 @@ jobs:
|
|||||||
|
|
||||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||||
|
|||||||
128
CLAUDE.md
128
CLAUDE.md
@@ -1,10 +1,11 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Development Tools
|
## Development Tools
|
||||||
|
|
||||||
- Runtime: Bun 1.2.11
|
- Runtime: Bun 1.2.11
|
||||||
|
- TypeScript with strict configuration
|
||||||
|
|
||||||
## Common Development Tasks
|
## Common Development Tasks
|
||||||
|
|
||||||
@@ -17,42 +18,119 @@ bun test
|
|||||||
# Formatting
|
# Formatting
|
||||||
bun run format # Format code with prettier
|
bun run format # Format code with prettier
|
||||||
bun run format:check # Check code formatting
|
bun run format:check # Check code formatting
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
bun run typecheck # Run TypeScript type checker
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action:
|
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action operates in two main phases:
|
||||||
|
|
||||||
1. **Trigger Detection**: Uses `check-trigger.ts` to determine if Claude should respond based on comment/issue content
|
### Phase 1: Preparation (`src/entrypoints/prepare.ts`)
|
||||||
2. **Context Gathering**: Fetches GitHub data (PRs, issues, comments) via `github-data-fetcher.ts` and formats it using `github-data-formatter.ts`
|
|
||||||
3. **AI Integration**: Supports multiple Claude providers (Anthropic API, AWS Bedrock, Google Vertex AI)
|
|
||||||
4. **Prompt Creation**: Generates context-rich prompts using `create-prompt.ts`
|
|
||||||
5. **MCP Server Integration**: Installs and configures GitHub MCP server for extended functionality
|
|
||||||
|
|
||||||
### Key Components
|
1. **Authentication Setup**: Establishes GitHub token via OIDC or GitHub App
|
||||||
|
2. **Permission Validation**: Verifies actor has write permissions
|
||||||
|
3. **Trigger Detection**: Uses mode-specific logic to determine if Claude should respond
|
||||||
|
4. **Context Creation**: Prepares GitHub context and initial tracking comment
|
||||||
|
|
||||||
- **Trigger System**: Responds to `/claude` comments or issue assignments
|
### Phase 2: Execution (`base-action/`)
|
||||||
- **Authentication**: OIDC-based token exchange for secure GitHub interactions
|
|
||||||
- **Cloud Integration**: Supports direct Anthropic API, AWS Bedrock, and Google Vertex AI
|
The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose:
|
||||||
- **GitHub Operations**: Creates branches, posts comments, and manages PRs/issues
|
|
||||||
|
- **Standalone Action**: Published separately as `@anthropic-ai/claude-code-base-action` for direct use
|
||||||
|
- **Inner Logic**: Used internally by this GitHub Action after preparation phase completes
|
||||||
|
|
||||||
|
Execution steps:
|
||||||
|
|
||||||
|
1. **MCP Server Setup**: Installs and configures GitHub MCP server for tool access
|
||||||
|
2. **Prompt Generation**: Creates context-rich prompts from GitHub data
|
||||||
|
3. **Claude Integration**: Executes via multiple providers (Anthropic API, AWS Bedrock, Google Vertex AI)
|
||||||
|
4. **Result Processing**: Updates comments and creates branches/PRs as needed
|
||||||
|
|
||||||
|
### Key Architectural Components
|
||||||
|
|
||||||
|
#### Mode System (`src/modes/`)
|
||||||
|
|
||||||
|
- **Tag Mode** (`tag/`): Responds to `@claude` mentions and issue assignments
|
||||||
|
- **Agent Mode** (`agent/`): Automated execution without trigger checking
|
||||||
|
- Extensible registry pattern in `modes/registry.ts`
|
||||||
|
|
||||||
|
#### GitHub Integration (`src/github/`)
|
||||||
|
|
||||||
|
- **Context Parsing** (`context.ts`): Unified GitHub event handling
|
||||||
|
- **Data Fetching** (`data/fetcher.ts`): Retrieves PR/issue data via GraphQL/REST
|
||||||
|
- **Data Formatting** (`data/formatter.ts`): Converts GitHub data to Claude-readable format
|
||||||
|
- **Branch Operations** (`operations/branch.ts`): Handles branch creation and cleanup
|
||||||
|
- **Comment Management** (`operations/comments/`): Creates and updates tracking comments
|
||||||
|
|
||||||
|
#### MCP Server Integration (`src/mcp/`)
|
||||||
|
|
||||||
|
- **GitHub Actions Server** (`github-actions-server.ts`): Workflow and CI access
|
||||||
|
- **GitHub Comment Server** (`github-comment-server.ts`): Comment operations
|
||||||
|
- **GitHub File Operations** (`github-file-ops-server.ts`): File system access
|
||||||
|
- Auto-installation and configuration in `install-mcp-server.ts`
|
||||||
|
|
||||||
|
#### Authentication & Security (`src/github/`)
|
||||||
|
|
||||||
|
- **Token Management** (`token.ts`): OIDC token exchange and GitHub App authentication
|
||||||
|
- **Permission Validation** (`validation/permissions.ts`): Write access verification
|
||||||
|
- **Actor Validation** (`validation/actor.ts`): Human vs bot detection
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── check-trigger.ts # Determines if Claude should respond
|
├── entrypoints/ # Action entry points
|
||||||
├── create-prompt.ts # Generates contextual prompts
|
│ ├── prepare.ts # Main preparation logic
|
||||||
├── github-data-fetcher.ts # Retrieves GitHub data
|
│ ├── update-comment-link.ts # Post-execution comment updates
|
||||||
├── github-data-formatter.ts # Formats GitHub data for prompts
|
│ └── format-turns.ts # Claude conversation formatting
|
||||||
├── install-mcp-server.ts # Sets up GitHub MCP server
|
├── github/ # GitHub integration layer
|
||||||
├── update-comment-with-link.ts # Updates comments with job links
|
│ ├── api/ # REST/GraphQL clients
|
||||||
└── types/
|
│ ├── data/ # Data fetching and formatting
|
||||||
└── github.ts # TypeScript types for GitHub data
|
│ ├── operations/ # Branch, comment, git operations
|
||||||
|
│ ├── validation/ # Permission and trigger validation
|
||||||
|
│ └── utils/ # Image downloading, sanitization
|
||||||
|
├── modes/ # Execution modes
|
||||||
|
│ ├── tag/ # @claude mention mode
|
||||||
|
│ ├── agent/ # Automation mode
|
||||||
|
│ └── registry.ts # Mode selection logic
|
||||||
|
├── mcp/ # MCP server implementations
|
||||||
|
├── prepare/ # Preparation orchestration
|
||||||
|
└── utils/ # Shared utilities
|
||||||
```
|
```
|
||||||
|
|
||||||
## Important Notes
|
## Important Implementation Notes
|
||||||
|
|
||||||
- Actions are triggered by `@claude` comments or issue assignment unless a different trigger_phrase is specified
|
### Authentication Flow
|
||||||
- The action creates branches for issues and pushes to PR branches directly
|
|
||||||
- All actions create OIDC tokens for secure authentication
|
- Uses GitHub OIDC token exchange for secure authentication
|
||||||
- Progress is tracked through dynamic comment updates with checkboxes
|
- Supports custom GitHub Apps via `APP_ID` and `APP_PRIVATE_KEY`
|
||||||
|
- Falls back to official Claude GitHub App if no custom app provided
|
||||||
|
|
||||||
|
### MCP Server Architecture
|
||||||
|
|
||||||
|
- Each MCP server has specific GitHub API access patterns
|
||||||
|
- Servers are auto-installed in `~/.claude/mcp/github-{type}-server/`
|
||||||
|
- Configuration merged with user-provided MCP config via `mcp_config` input
|
||||||
|
|
||||||
|
### Mode System Design
|
||||||
|
|
||||||
|
- Modes implement `Mode` interface with `shouldTrigger()` and `prepare()` methods
|
||||||
|
- Registry validates mode compatibility with GitHub event types
|
||||||
|
- Agent mode bypasses all trigger checking for automation scenarios
|
||||||
|
|
||||||
|
### Comment Threading
|
||||||
|
|
||||||
|
- Single tracking comment updated throughout execution
|
||||||
|
- Progress indicated via dynamic checkboxes
|
||||||
|
- Links to job runs and created branches/PRs
|
||||||
|
- Sticky comment option for consolidated PR comments
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
- Use Bun-specific TypeScript configuration with `moduleResolution: "bundler"`
|
||||||
|
- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` enabled
|
||||||
|
- Prefer explicit error handling with detailed error messages
|
||||||
|
- Use discriminated unions for GitHub context types
|
||||||
|
- Implement retry logic for GitHub API operations via `utils/retry.ts`
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
bun install -g @anthropic-ai/claude-code@1.0.62
|
bun install -g @anthropic-ai/claude-code@1.0.64
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bun install -g @anthropic-ai/claude-code@1.0.62
|
run: bun install -g @anthropic-ai/claude-code@1.0.64
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
|
|
||||||
Provide constructive feedback with specific suggestions for improvement.
|
Provide constructive feedback with specific suggestions for improvement.
|
||||||
Use inline comments to highlight specific areas of concern.
|
Use inline comments to highlight specific areas of concern.
|
||||||
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||||
|
|||||||
40
examples/workflow-dispatch-agent.yml
Normal file
40
examples/workflow-dispatch-agent.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Claude Commit Analysis
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
analysis_type:
|
||||||
|
description: "Type of analysis to perform"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- summarize-commit
|
||||||
|
- security-review
|
||||||
|
default: "summarize-commit"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||||
|
|
||||||
|
- name: Run Claude Analysis
|
||||||
|
uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
mode: agent
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
override_prompt: |
|
||||||
|
Analyze the latest commit in this repository.
|
||||||
|
|
||||||
|
${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }}
|
||||||
|
|
||||||
|
${{ github.event.inputs.analysis_type == 'security-review' && 'Task: Review the latest commit for potential security vulnerabilities. Check for exposed secrets, insecure coding patterns, dependency vulnerabilities, or any other security concerns. Provide specific recommendations if issues are found.' || '' }}
|
||||||
@@ -587,23 +587,28 @@ ${formattedBody}
|
|||||||
${formattedComments || "No comments"}
|
${formattedComments || "No comments"}
|
||||||
</comments>
|
</comments>
|
||||||
|
|
||||||
<review_comments>
|
${
|
||||||
${eventData.isPR ? formattedReviewComments || "No review comments" : ""}
|
eventData.isPR
|
||||||
</review_comments>
|
? `<review_comments>
|
||||||
|
${formattedReviewComments || "No review comments"}
|
||||||
|
</review_comments>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
<changed_files>
|
${
|
||||||
${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
|
eventData.isPR
|
||||||
</changed_files>${imagesInfo}
|
? `<changed_files>
|
||||||
|
${formattedChangedFiles || "No files changed"}
|
||||||
|
</changed_files>`
|
||||||
|
: ""
|
||||||
|
}${imagesInfo}
|
||||||
|
|
||||||
<event_type>${eventType}</event_type>
|
<event_type>${eventType}</event_type>
|
||||||
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
|
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
|
||||||
<trigger_context>${triggerContext}</trigger_context>
|
<trigger_context>${triggerContext}</trigger_context>
|
||||||
<repository>${context.repository}</repository>
|
<repository>${context.repository}</repository>
|
||||||
${
|
${eventData.isPR && eventData.prNumber ? `<pr_number>${eventData.prNumber}</pr_number>` : ""}
|
||||||
eventData.isPR
|
${!eventData.isPR && eventData.issueNumber ? `<issue_number>${eventData.issueNumber}</issue_number>` : ""}
|
||||||
? `<pr_number>${eventData.prNumber}</pr_number>`
|
|
||||||
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
|
|
||||||
}
|
|
||||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||||
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
||||||
@@ -801,15 +806,18 @@ export async function createPrompt(
|
|||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Tag mode requires a comment ID
|
// Prepare the context for prompt generation
|
||||||
if (mode.name === "tag" && !modeContext.commentId) {
|
let claudeCommentId: string = "";
|
||||||
|
if (mode.name === "tag") {
|
||||||
|
if (!modeContext.commentId) {
|
||||||
throw new Error("Tag mode requires a comment ID for prompt generation");
|
throw new Error("Tag mode requires a comment ID for prompt generation");
|
||||||
}
|
}
|
||||||
|
claudeCommentId = modeContext.commentId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the context for prompt generation
|
|
||||||
const preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
context,
|
context,
|
||||||
modeContext.commentId?.toString() || "",
|
claudeCommentId,
|
||||||
modeContext.baseBranch,
|
modeContext.baseBranch,
|
||||||
modeContext.claudeBranch,
|
modeContext.claudeBranch,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
|
|||||||
markdown += "---\n\n";
|
markdown += "---\n\n";
|
||||||
} else if (itemType === "final_result") {
|
} else if (itemType === "final_result") {
|
||||||
const data = item.data || {};
|
const data = item.data || {};
|
||||||
const cost = (data as any).cost_usd || 0;
|
const cost = (data as any).total_cost_usd || (data as any).cost_usd || 0;
|
||||||
const duration = (data as any).duration_ms || 0;
|
const duration = (data as any).duration_ms || 0;
|
||||||
const resultText = (data as any).result || "";
|
const resultText = (data as any).result || "";
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,11 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { setupGitHubToken } from "../github/token";
|
import { setupGitHubToken } from "../github/token";
|
||||||
import { checkHumanActor } from "../github/validation/actor";
|
|
||||||
import { checkWritePermissions } from "../github/validation/permissions";
|
import { checkWritePermissions } from "../github/validation/permissions";
|
||||||
import { createInitialComment } from "../github/operations/comments/create-initial";
|
|
||||||
import { setupBranch } from "../github/operations/branch";
|
|
||||||
import { configureGitAuth } from "../github/operations/git-config";
|
|
||||||
import { prepareMcpConfig } from "../mcp/install-mcp-server";
|
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
import { fetchGitHubData } from "../github/data/fetcher";
|
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||||
import { parseGitHubContext } from "../github/context";
|
|
||||||
import { getMode } from "../modes/registry";
|
import { getMode } from "../modes/registry";
|
||||||
import { createPrompt } from "../create-prompt";
|
import { prepare } from "../prepare";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
@@ -28,7 +22,8 @@ async function run() {
|
|||||||
// Step 2: Parse GitHub context (once for all operations)
|
// Step 2: Parse GitHub context (once for all operations)
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
|
|
||||||
// Step 3: Check write permissions
|
// Step 3: Check write permissions (only for entity contexts)
|
||||||
|
if (isEntityContext(context)) {
|
||||||
const hasWritePermissions = await checkWritePermissions(
|
const hasWritePermissions = await checkWritePermissions(
|
||||||
octokit.rest,
|
octokit.rest,
|
||||||
context,
|
context,
|
||||||
@@ -38,9 +33,10 @@ async function run() {
|
|||||||
"Actor does not have write permissions to the repository",
|
"Actor does not have write permissions to the repository",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 4: Get mode and check trigger conditions
|
// Step 4: Get mode and check trigger conditions
|
||||||
const mode = getMode(context.inputs.mode);
|
const mode = getMode(context.inputs.mode, context);
|
||||||
const containsTrigger = mode.shouldTrigger(context);
|
const containsTrigger = mode.shouldTrigger(context);
|
||||||
|
|
||||||
// Set output for action.yml to check
|
// Set output for action.yml to check
|
||||||
@@ -51,65 +47,16 @@ async function run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Check if actor is human
|
// Step 5: Use the new modular prepare function
|
||||||
await checkHumanActor(octokit.rest, context);
|
const result = await prepare({
|
||||||
|
|
||||||
// Step 6: Create initial tracking comment (mode-aware)
|
|
||||||
// Some modes (e.g., agent mode) may not need tracking comments
|
|
||||||
let commentId: number | undefined;
|
|
||||||
let commentData:
|
|
||||||
| Awaited<ReturnType<typeof createInitialComment>>
|
|
||||||
| undefined;
|
|
||||||
if (mode.shouldCreateTrackingComment()) {
|
|
||||||
commentData = await createInitialComment(octokit.rest, context);
|
|
||||||
commentId = commentData.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
|
|
||||||
const githubData = await fetchGitHubData({
|
|
||||||
octokits: octokit,
|
|
||||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
|
||||||
prNumber: context.entityNumber.toString(),
|
|
||||||
isPR: context.isPR,
|
|
||||||
triggerUsername: context.actor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 8: Setup branch
|
|
||||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
|
||||||
|
|
||||||
// Step 9: Configure git authentication if not using commit signing
|
|
||||||
if (!context.inputs.useCommitSigning) {
|
|
||||||
try {
|
|
||||||
await configureGitAuth(githubToken, context, commentData?.user || null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to configure git authentication:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 10: Create prompt file
|
|
||||||
const modeContext = mode.prepareContext(context, {
|
|
||||||
commentId,
|
|
||||||
baseBranch: branchInfo.baseBranch,
|
|
||||||
claudeBranch: branchInfo.claudeBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createPrompt(mode, modeContext, githubData, context);
|
|
||||||
|
|
||||||
// Step 11: Get MCP configuration
|
|
||||||
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
|
||||||
const mcpConfig = await prepareMcpConfig({
|
|
||||||
githubToken,
|
|
||||||
owner: context.repository.owner,
|
|
||||||
repo: context.repository.repo,
|
|
||||||
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
|
||||||
baseBranch: branchInfo.baseBranch,
|
|
||||||
additionalMcpConfig,
|
|
||||||
claudeCommentId: commentId?.toString() || "",
|
|
||||||
allowedTools: context.inputs.allowedTools,
|
|
||||||
context,
|
context,
|
||||||
|
octokit,
|
||||||
|
mode,
|
||||||
|
githubToken,
|
||||||
});
|
});
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
|
||||||
|
// Set the MCP config output
|
||||||
|
core.setOutput("mcp_config", result.mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
parseGitHubContext,
|
parseGitHubContext,
|
||||||
isPullRequestReviewCommentEvent,
|
isPullRequestReviewCommentEvent,
|
||||||
|
isEntityContext,
|
||||||
} from "../github/context";
|
} from "../github/context";
|
||||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||||
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
||||||
@@ -23,7 +24,14 @@ async function run() {
|
|||||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||||
|
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
|
|
||||||
|
// This script is only called for entity-based events
|
||||||
|
if (!isEntityContext(context)) {
|
||||||
|
throw new Error("update-comment-link requires an entity context");
|
||||||
|
}
|
||||||
|
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
|
|
||||||
const octokit = createOctokit(githubToken);
|
const octokit = createOctokit(githubToken);
|
||||||
|
|
||||||
const serverUrl = GITHUB_SERVER_URL;
|
const serverUrl = GITHUB_SERVER_URL;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const PR_QUERY = `
|
|||||||
login
|
login
|
||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
|
isMinimized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reviews(first: 100) {
|
reviews(first: 100) {
|
||||||
@@ -69,6 +70,7 @@ export const PR_QUERY = `
|
|||||||
login
|
login
|
||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
|
isMinimized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +100,7 @@ export const ISSUE_QUERY = `
|
|||||||
login
|
login
|
||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
|
isMinimized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,54 @@ import type {
|
|||||||
PullRequestReviewEvent,
|
PullRequestReviewEvent,
|
||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
|
// Custom types for GitHub Actions events that aren't webhooks
|
||||||
|
export type WorkflowDispatchEvent = {
|
||||||
|
action?: never;
|
||||||
|
inputs?: Record<string, any>;
|
||||||
|
ref?: string;
|
||||||
|
repository: {
|
||||||
|
name: string;
|
||||||
|
owner: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sender: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
workflow: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScheduleEvent = {
|
||||||
|
action?: never;
|
||||||
|
schedule?: string;
|
||||||
|
repository: {
|
||||||
|
name: string;
|
||||||
|
owner: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
import type { ModeName } from "../modes/types";
|
import type { ModeName } from "../modes/types";
|
||||||
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
||||||
|
|
||||||
export type ParsedGitHubContext = {
|
// Event name constants for better maintainability
|
||||||
|
const ENTITY_EVENT_NAMES = [
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const AUTOMATION_EVENT_NAMES = ["workflow_dispatch", "schedule"] as const;
|
||||||
|
|
||||||
|
// Derive types from constants for better maintainability
|
||||||
|
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
|
||||||
|
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
|
||||||
|
|
||||||
|
// Common fields shared by all context types
|
||||||
|
type BaseContext = {
|
||||||
runId: string;
|
runId: string;
|
||||||
eventName: string;
|
|
||||||
eventAction?: string;
|
eventAction?: string;
|
||||||
repository: {
|
repository: {
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -20,14 +62,6 @@ export type ParsedGitHubContext = {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
};
|
};
|
||||||
actor: string;
|
actor: string;
|
||||||
payload:
|
|
||||||
| IssuesEvent
|
|
||||||
| IssueCommentEvent
|
|
||||||
| PullRequestEvent
|
|
||||||
| PullRequestReviewEvent
|
|
||||||
| PullRequestReviewCommentEvent;
|
|
||||||
entityNumber: number;
|
|
||||||
isPR: boolean;
|
|
||||||
inputs: {
|
inputs: {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
triggerPhrase: string;
|
triggerPhrase: string;
|
||||||
@@ -46,7 +80,29 @@ export type ParsedGitHubContext = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseGitHubContext(): ParsedGitHubContext {
|
// Context for entity-based events (issues, PRs, comments)
|
||||||
|
export type ParsedGitHubContext = BaseContext & {
|
||||||
|
eventName: EntityEventName;
|
||||||
|
payload:
|
||||||
|
| IssuesEvent
|
||||||
|
| IssueCommentEvent
|
||||||
|
| PullRequestEvent
|
||||||
|
| PullRequestReviewEvent
|
||||||
|
| PullRequestReviewCommentEvent;
|
||||||
|
entityNumber: number;
|
||||||
|
isPR: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Context for automation events (workflow_dispatch, schedule)
|
||||||
|
export type AutomationContext = BaseContext & {
|
||||||
|
eventName: AutomationEventName;
|
||||||
|
payload: WorkflowDispatchEvent | ScheduleEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Union type for all contexts
|
||||||
|
export type GitHubContext = ParsedGitHubContext | AutomationContext;
|
||||||
|
|
||||||
|
export function parseGitHubContext(): GitHubContext {
|
||||||
const context = github.context;
|
const context = github.context;
|
||||||
|
|
||||||
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
||||||
@@ -56,7 +112,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
|
|
||||||
const commonFields = {
|
const commonFields = {
|
||||||
runId: process.env.GITHUB_RUN_ID!,
|
runId: process.env.GITHUB_RUN_ID!,
|
||||||
eventName: context.eventName,
|
|
||||||
eventAction: context.payload.action,
|
eventAction: context.payload.action,
|
||||||
repository: {
|
repository: {
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -86,49 +141,69 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
|
|
||||||
switch (context.eventName) {
|
switch (context.eventName) {
|
||||||
case "issues": {
|
case "issues": {
|
||||||
|
const payload = context.payload as IssuesEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as IssuesEvent,
|
eventName: "issues",
|
||||||
entityNumber: (context.payload as IssuesEvent).issue.number,
|
payload,
|
||||||
|
entityNumber: payload.issue.number,
|
||||||
isPR: false,
|
isPR: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "issue_comment": {
|
case "issue_comment": {
|
||||||
|
const payload = context.payload as IssueCommentEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as IssueCommentEvent,
|
eventName: "issue_comment",
|
||||||
entityNumber: (context.payload as IssueCommentEvent).issue.number,
|
payload,
|
||||||
isPR: Boolean(
|
entityNumber: payload.issue.number,
|
||||||
(context.payload as IssueCommentEvent).issue.pull_request,
|
isPR: Boolean(payload.issue.pull_request),
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request": {
|
case "pull_request": {
|
||||||
|
const payload = context.payload as PullRequestEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestEvent,
|
eventName: "pull_request",
|
||||||
entityNumber: (context.payload as PullRequestEvent).pull_request.number,
|
payload,
|
||||||
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request_review": {
|
case "pull_request_review": {
|
||||||
|
const payload = context.payload as PullRequestReviewEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestReviewEvent,
|
eventName: "pull_request_review",
|
||||||
entityNumber: (context.payload as PullRequestReviewEvent).pull_request
|
payload,
|
||||||
.number,
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request_review_comment": {
|
case "pull_request_review_comment": {
|
||||||
|
const payload = context.payload as PullRequestReviewCommentEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestReviewCommentEvent,
|
eventName: "pull_request_review_comment",
|
||||||
entityNumber: (context.payload as PullRequestReviewCommentEvent)
|
payload,
|
||||||
.pull_request.number,
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "workflow_dispatch": {
|
||||||
|
return {
|
||||||
|
...commonFields,
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
payload: context.payload as unknown as WorkflowDispatchEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "schedule": {
|
||||||
|
return {
|
||||||
|
...commonFields,
|
||||||
|
eventName: "schedule",
|
||||||
|
payload: context.payload as unknown as ScheduleEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported event type: ${context.eventName}`);
|
throw new Error(`Unsupported event type: ${context.eventName}`);
|
||||||
}
|
}
|
||||||
@@ -162,37 +237,53 @@ export function parseAdditionalPermissions(s: string): Map<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isIssuesEvent(
|
export function isIssuesEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
||||||
return context.eventName === "issues";
|
return context.eventName === "issues";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIssueCommentEvent(
|
export function isIssueCommentEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
||||||
return context.eventName === "issue_comment";
|
return context.eventName === "issue_comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestEvent(
|
export function isPullRequestEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
||||||
return context.eventName === "pull_request";
|
return context.eventName === "pull_request";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestReviewEvent(
|
export function isPullRequestReviewEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
||||||
return context.eventName === "pull_request_review";
|
return context.eventName === "pull_request_review";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestReviewCommentEvent(
|
export function isPullRequestReviewCommentEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
||||||
return context.eventName === "pull_request_review_comment";
|
return context.eventName === "pull_request_review_comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIssuesAssignedEvent(
|
export function isIssuesAssignedEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
||||||
return isIssuesEvent(context) && context.eventAction === "assigned";
|
return isIssuesEvent(context) && context.eventAction === "assigned";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type guard to check if context is an entity context (has entityNumber and isPR)
|
||||||
|
export function isEntityContext(
|
||||||
|
context: GitHubContext,
|
||||||
|
): context is ParsedGitHubContext {
|
||||||
|
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to check if context is an automation context
|
||||||
|
export function isAutomationContext(
|
||||||
|
context: GitHubContext,
|
||||||
|
): context is AutomationContext {
|
||||||
|
return AUTOMATION_EVENT_NAMES.includes(
|
||||||
|
context.eventName as AutomationEventName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export async function fetchGitHubData({
|
|||||||
|
|
||||||
// Prepare all comments for image processing
|
// Prepare all comments for image processing
|
||||||
const issueComments: CommentWithImages[] = comments
|
const issueComments: CommentWithImages[] = comments
|
||||||
.filter((c) => c.body)
|
.filter((c) => c.body && !c.isMinimized)
|
||||||
.map((c) => ({
|
.map((c) => ({
|
||||||
type: "issue_comment" as const,
|
type: "issue_comment" as const,
|
||||||
id: c.databaseId,
|
id: c.databaseId,
|
||||||
@@ -154,7 +154,7 @@ export async function fetchGitHubData({
|
|||||||
const reviewComments: CommentWithImages[] =
|
const reviewComments: CommentWithImages[] =
|
||||||
reviewData?.nodes
|
reviewData?.nodes
|
||||||
?.flatMap((r) => r.comments?.nodes ?? [])
|
?.flatMap((r) => r.comments?.nodes ?? [])
|
||||||
.filter((c) => c.body)
|
.filter((c) => c.body && !c.isMinimized)
|
||||||
.map((c) => ({
|
.map((c) => ({
|
||||||
type: "review_comment" as const,
|
type: "review_comment" as const,
|
||||||
id: c.databaseId,
|
id: c.databaseId,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function formatComments(
|
|||||||
imageUrlMap?: Map<string, string>,
|
imageUrlMap?: Map<string, string>,
|
||||||
): string {
|
): string {
|
||||||
return comments
|
return comments
|
||||||
|
.filter((comment) => !comment.isMinimized)
|
||||||
.map((comment) => {
|
.map((comment) => {
|
||||||
let body = comment.body;
|
let body = comment.body;
|
||||||
|
|
||||||
@@ -96,6 +97,7 @@ export function formatReviewComments(
|
|||||||
review.comments.nodes.length > 0
|
review.comments.nodes.length > 0
|
||||||
) {
|
) {
|
||||||
const comments = review.comments.nodes
|
const comments = review.comments.nodes
|
||||||
|
.filter((comment) => !comment.isMinimized)
|
||||||
.map((comment) => {
|
.map((comment) => {
|
||||||
let body = comment.body;
|
let body = comment.body;
|
||||||
|
|
||||||
@@ -110,8 +112,10 @@ export function formatReviewComments(
|
|||||||
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
if (comments) {
|
||||||
reviewOutput += `\n${comments}`;
|
reviewOutput += `\n${comments}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reviewOutput;
|
return reviewOutput;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type GitHubComment = {
|
|||||||
body: string;
|
body: string;
|
||||||
author: GitHubAuthor;
|
author: GitHubAuthor;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
isMinimized?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubReviewComment = GitHubComment & {
|
export type GitHubReviewComment = GitHubComment & {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
|
||||||
import type { ParsedGitHubContext } from "../github/context";
|
import type { ParsedGitHubContext } from "../github/context";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ export async function prepareMcpConfig(
|
|||||||
GITHUB_TOKEN: process.env.ACTIONS_TOKEN,
|
GITHUB_TOKEN: process.env.ACTIONS_TOKEN,
|
||||||
REPO_OWNER: owner,
|
REPO_OWNER: owner,
|
||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
PR_NUMBER: context.entityNumber.toString(),
|
PR_NUMBER: context.entityNumber?.toString() || "",
|
||||||
RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp",
|
RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -156,10 +156,13 @@ export async function prepareMcpConfig(
|
|||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
|
"-e",
|
||||||
|
"GITHUB_HOST",
|
||||||
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
|
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||||
|
GITHUB_HOST: GITHUB_SERVER_URL,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
import type { Mode } from "../types";
|
import * as core from "@actions/core";
|
||||||
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
|
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||||
|
import { isAutomationContext } from "../../github/context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent mode implementation.
|
* Agent mode implementation.
|
||||||
*
|
*
|
||||||
* This mode is designed for automation and workflow_dispatch scenarios.
|
* This mode is specifically designed for automation events (workflow_dispatch and schedule).
|
||||||
* It always triggers (no checking), allows highly flexible configurations,
|
* It bypasses the standard trigger checking and comment tracking used by tag mode,
|
||||||
* and works well with override_prompt for custom workflows.
|
* making it ideal for scheduled tasks and manual workflow runs.
|
||||||
*
|
|
||||||
* In the future, this mode could restrict certain tools for safety in automation contexts,
|
|
||||||
* e.g., disallowing WebSearch or limiting file system operations.
|
|
||||||
*/
|
*/
|
||||||
export const agentMode: Mode = {
|
export const agentMode: Mode = {
|
||||||
name: "agent",
|
name: "agent",
|
||||||
description: "Automation mode that always runs without trigger checking",
|
description: "Automation mode for workflow_dispatch and schedule events",
|
||||||
|
|
||||||
shouldTrigger() {
|
shouldTrigger(context) {
|
||||||
return true;
|
// Only trigger for automation events
|
||||||
|
return isAutomationContext(context);
|
||||||
},
|
},
|
||||||
|
|
||||||
prepareContext(context, data) {
|
prepareContext(context) {
|
||||||
|
// Agent mode doesn't use comment tracking or branch management
|
||||||
return {
|
return {
|
||||||
mode: "agent",
|
mode: "agent",
|
||||||
githubContext: context,
|
githubContext: context,
|
||||||
commentId: data?.commentId,
|
|
||||||
baseBranch: data?.baseBranch,
|
|
||||||
claudeBranch: data?.claudeBranch,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,4 +38,80 @@ export const agentMode: Mode = {
|
|||||||
shouldCreateTrackingComment() {
|
shouldCreateTrackingComment() {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async prepare({ context }: ModeOptions): Promise<ModeResult> {
|
||||||
|
// Agent mode handles automation events (workflow_dispatch, schedule) only
|
||||||
|
|
||||||
|
// Create prompt directory
|
||||||
|
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the prompt file - the base action requires a prompt_file parameter,
|
||||||
|
// so we must create this file even though agent mode typically uses
|
||||||
|
// override_prompt or direct_prompt. If neither is provided, we write
|
||||||
|
// a minimal prompt with just the repository information.
|
||||||
|
const promptContent =
|
||||||
|
context.inputs.overridePrompt ||
|
||||||
|
context.inputs.directPrompt ||
|
||||||
|
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
||||||
|
promptContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export tool environment variables for agent mode
|
||||||
|
const baseTools = [
|
||||||
|
"Edit",
|
||||||
|
"MultiEdit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"LS",
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add user-specified tools
|
||||||
|
const allowedTools = [...baseTools, ...context.inputs.allowedTools];
|
||||||
|
const disallowedTools = [
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
...context.inputs.disallowedTools,
|
||||||
|
];
|
||||||
|
|
||||||
|
core.exportVariable("ALLOWED_TOOLS", allowedTools.join(","));
|
||||||
|
core.exportVariable("DISALLOWED_TOOLS", disallowedTools.join(","));
|
||||||
|
|
||||||
|
// Agent mode uses a minimal MCP configuration
|
||||||
|
// We don't need comment servers or PR-specific tools for automation
|
||||||
|
const mcpConfig: any = {
|
||||||
|
mcpServers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add user-provided additional MCP config if any
|
||||||
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
|
if (additionalMcpConfig.trim()) {
|
||||||
|
try {
|
||||||
|
const additional = JSON.parse(additionalMcpConfig);
|
||||||
|
if (additional && typeof additional === "object") {
|
||||||
|
Object.assign(mcpConfig, additional);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to parse additional MCP config: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput("mcp_config", JSON.stringify(mcpConfig));
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentId: undefined,
|
||||||
|
branchInfo: {
|
||||||
|
baseBranch: "",
|
||||||
|
currentBranch: "",
|
||||||
|
claudeBranch: undefined,
|
||||||
|
},
|
||||||
|
mcpConfig: JSON.stringify(mcpConfig),
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
import type { Mode, ModeName } from "./types";
|
import type { Mode, ModeName } from "./types";
|
||||||
import { tagMode } from "./tag";
|
import { tagMode } from "./tag";
|
||||||
import { agentMode } from "./agent";
|
import { agentMode } from "./agent";
|
||||||
|
import type { GitHubContext } from "../github/context";
|
||||||
|
import { isAutomationContext } from "../github/context";
|
||||||
|
|
||||||
export const DEFAULT_MODE = "tag" as const;
|
export const DEFAULT_MODE = "tag" as const;
|
||||||
export const VALID_MODES = ["tag", "agent"] as const;
|
export const VALID_MODES = ["tag", "agent"] as const;
|
||||||
@@ -27,12 +29,13 @@ const modes = {
|
|||||||
} as const satisfies Record<ModeName, Mode>;
|
} as const satisfies Record<ModeName, Mode>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a mode by name.
|
* Retrieves a mode by name and validates it can handle the event type.
|
||||||
* @param name The mode name to retrieve
|
* @param name The mode name to retrieve
|
||||||
|
* @param context The GitHub context to validate against
|
||||||
* @returns The requested mode
|
* @returns The requested mode
|
||||||
* @throws Error if the mode is not found
|
* @throws Error if the mode is not found or cannot handle the event
|
||||||
*/
|
*/
|
||||||
export function getMode(name: ModeName): Mode {
|
export function getMode(name: ModeName, context: GitHubContext): Mode {
|
||||||
const mode = modes[name];
|
const mode = modes[name];
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
const validModes = VALID_MODES.join("', '");
|
const validModes = VALID_MODES.join("', '");
|
||||||
@@ -40,6 +43,14 @@ export function getMode(name: ModeName): Mode {
|
|||||||
`Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`,
|
`Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate mode can handle the event type
|
||||||
|
if (name === "tag" && isAutomationContext(context)) {
|
||||||
|
throw new Error(
|
||||||
|
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import type { Mode } from "../types";
|
import * as core from "@actions/core";
|
||||||
|
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||||
import { checkContainsTrigger } from "../../github/validation/trigger";
|
import { checkContainsTrigger } from "../../github/validation/trigger";
|
||||||
|
import { checkHumanActor } from "../../github/validation/actor";
|
||||||
|
import { createInitialComment } from "../../github/operations/comments/create-initial";
|
||||||
|
import { setupBranch } from "../../github/operations/branch";
|
||||||
|
import { configureGitAuth } from "../../github/operations/git-config";
|
||||||
|
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||||
|
import { fetchGitHubData } from "../../github/data/fetcher";
|
||||||
|
import { createPrompt } from "../../create-prompt";
|
||||||
|
import { isEntityContext } from "../../github/context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag mode implementation.
|
* Tag mode implementation.
|
||||||
@@ -13,6 +22,10 @@ export const tagMode: Mode = {
|
|||||||
description: "Traditional implementation mode triggered by @claude mentions",
|
description: "Traditional implementation mode triggered by @claude mentions",
|
||||||
|
|
||||||
shouldTrigger(context) {
|
shouldTrigger(context) {
|
||||||
|
// Tag mode only handles entity events
|
||||||
|
if (!isEntityContext(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return checkContainsTrigger(context);
|
return checkContainsTrigger(context);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -37,4 +50,74 @@ export const tagMode: Mode = {
|
|||||||
shouldCreateTrackingComment() {
|
shouldCreateTrackingComment() {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async prepare({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
githubToken,
|
||||||
|
}: ModeOptions): Promise<ModeResult> {
|
||||||
|
// Tag mode only handles entity-based events
|
||||||
|
if (!isEntityContext(context)) {
|
||||||
|
throw new Error("Tag mode requires entity context");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if actor is human
|
||||||
|
await checkHumanActor(octokit.rest, context);
|
||||||
|
|
||||||
|
// Create initial tracking comment
|
||||||
|
const commentData = await createInitialComment(octokit.rest, context);
|
||||||
|
const commentId = commentData.id;
|
||||||
|
|
||||||
|
const githubData = await fetchGitHubData({
|
||||||
|
octokits: octokit,
|
||||||
|
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||||
|
prNumber: context.entityNumber.toString(),
|
||||||
|
isPR: context.isPR,
|
||||||
|
triggerUsername: context.actor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup branch
|
||||||
|
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||||
|
|
||||||
|
// Configure git authentication if not using commit signing
|
||||||
|
if (!context.inputs.useCommitSigning) {
|
||||||
|
try {
|
||||||
|
await configureGitAuth(githubToken, context, commentData.user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to configure git authentication:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create prompt file
|
||||||
|
const modeContext = this.prepareContext(context, {
|
||||||
|
commentId,
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
claudeBranch: branchInfo.claudeBranch,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPrompt(tagMode, modeContext, githubData, context);
|
||||||
|
|
||||||
|
// Get MCP configuration
|
||||||
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
|
const mcpConfig = await prepareMcpConfig({
|
||||||
|
githubToken,
|
||||||
|
owner: context.repository.owner,
|
||||||
|
repo: context.repository.repo,
|
||||||
|
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
additionalMcpConfig,
|
||||||
|
claudeCommentId: commentId.toString(),
|
||||||
|
allowedTools: context.inputs.allowedTools,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentId,
|
||||||
|
branchInfo,
|
||||||
|
mcpConfig,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { ParsedGitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
|
|
||||||
export type ModeName = "tag" | "agent";
|
export type ModeName = "tag" | "agent";
|
||||||
|
|
||||||
export type ModeContext = {
|
export type ModeContext = {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
githubContext: ParsedGitHubContext;
|
githubContext: GitHubContext;
|
||||||
commentId?: number;
|
commentId?: number;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
@@ -32,12 +32,12 @@ export type Mode = {
|
|||||||
/**
|
/**
|
||||||
* Determines if this mode should trigger based on the GitHub context
|
* Determines if this mode should trigger based on the GitHub context
|
||||||
*/
|
*/
|
||||||
shouldTrigger(context: ParsedGitHubContext): boolean;
|
shouldTrigger(context: GitHubContext): boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the mode context with any additional data needed for prompt generation
|
* Prepares the mode context with any additional data needed for prompt generation
|
||||||
*/
|
*/
|
||||||
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
|
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of tools that should be allowed for this mode
|
* Returns the list of tools that should be allowed for this mode
|
||||||
@@ -53,4 +53,28 @@ export type Mode = {
|
|||||||
* Determines if this mode should create a tracking comment
|
* Determines if this mode should create a tracking comment
|
||||||
*/
|
*/
|
||||||
shouldCreateTrackingComment(): boolean;
|
shouldCreateTrackingComment(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the GitHub environment for this mode.
|
||||||
|
* Each mode decides how to handle different event types.
|
||||||
|
* @returns PrepareResult with commentId, branchInfo, and mcpConfig
|
||||||
|
*/
|
||||||
|
prepare(options: ModeOptions): Promise<ModeResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define types for mode prepare method to avoid circular dependencies
|
||||||
|
export type ModeOptions = {
|
||||||
|
context: GitHubContext;
|
||||||
|
octokit: any; // We'll use any to avoid circular dependency with Octokits
|
||||||
|
githubToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModeResult = {
|
||||||
|
commentId?: number;
|
||||||
|
branchInfo: {
|
||||||
|
baseBranch: string;
|
||||||
|
claudeBranch?: string;
|
||||||
|
currentBranch: string;
|
||||||
|
};
|
||||||
|
mcpConfig: string;
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/prepare/index.ts
Normal file
20
src/prepare/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Main prepare module that delegates to the mode's prepare method
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PrepareOptions, PrepareResult } from "./types";
|
||||||
|
|
||||||
|
export async function prepare(options: PrepareOptions): Promise<PrepareResult> {
|
||||||
|
const { mode, context, octokit, githubToken } = options;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Preparing with mode: ${mode.name} for event: ${context.eventName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delegate to the mode's prepare method
|
||||||
|
return mode.prepare({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
githubToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
src/prepare/types.ts
Normal file
20
src/prepare/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GitHubContext } from "../github/context";
|
||||||
|
import type { Octokits } from "../github/api/client";
|
||||||
|
import type { Mode } from "../modes/types";
|
||||||
|
|
||||||
|
export type PrepareResult = {
|
||||||
|
commentId?: number;
|
||||||
|
branchInfo: {
|
||||||
|
baseBranch: string;
|
||||||
|
claudeBranch?: string;
|
||||||
|
currentBranch: string;
|
||||||
|
};
|
||||||
|
mcpConfig: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrepareOptions = {
|
||||||
|
context: GitHubContext;
|
||||||
|
octokit: Octokits;
|
||||||
|
mode: Mode;
|
||||||
|
githubToken: string;
|
||||||
|
};
|
||||||
@@ -252,6 +252,63 @@ describe("formatComments", () => {
|
|||||||
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("filters out minimized comments", () => {
|
||||||
|
const comments: GitHubComment[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
databaseId: "100001",
|
||||||
|
body: "Normal comment",
|
||||||
|
author: { login: "user1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
isMinimized: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
databaseId: "100002",
|
||||||
|
body: "Minimized comment",
|
||||||
|
author: { login: "user2" },
|
||||||
|
createdAt: "2023-01-02T00:00:00Z",
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
databaseId: "100003",
|
||||||
|
body: "Another normal comment",
|
||||||
|
author: { login: "user3" },
|
||||||
|
createdAt: "2023-01-03T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatComments(comments);
|
||||||
|
expect(result).toBe(
|
||||||
|
`[user1 at 2023-01-01T00:00:00Z]: Normal comment\n\n[user3 at 2023-01-03T00:00:00Z]: Another normal comment`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty string when all comments are minimized", () => {
|
||||||
|
const comments: GitHubComment[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
databaseId: "100001",
|
||||||
|
body: "Minimized comment 1",
|
||||||
|
author: { login: "user1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
databaseId: "100002",
|
||||||
|
body: "Minimized comment 2",
|
||||||
|
author: { login: "user2" },
|
||||||
|
createdAt: "2023-01-02T00:00:00Z",
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatComments(comments);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("formatReviewComments", () => {
|
describe("formatReviewComments", () => {
|
||||||
@@ -517,6 +574,159 @@ describe("formatReviewComments", () => {
|
|||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: `,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("filters out minimized review comments", () => {
|
||||||
|
const reviewData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "review1",
|
||||||
|
databaseId: "300001",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
body: "Review with mixed comments",
|
||||||
|
state: "APPROVED",
|
||||||
|
submittedAt: "2023-01-01T00:00:00Z",
|
||||||
|
comments: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "comment1",
|
||||||
|
databaseId: "200001",
|
||||||
|
body: "Normal review comment",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/index.ts",
|
||||||
|
line: 42,
|
||||||
|
isMinimized: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comment2",
|
||||||
|
databaseId: "200002",
|
||||||
|
body: "Minimized review comment",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/utils.ts",
|
||||||
|
line: 15,
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comment3",
|
||||||
|
databaseId: "200003",
|
||||||
|
body: "Another normal comment",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/main.ts",
|
||||||
|
line: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatReviewComments(reviewData);
|
||||||
|
expect(result).toBe(
|
||||||
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with mixed comments\n [Comment on src/index.ts:42]: Normal review comment\n [Comment on src/main.ts:10]: Another normal comment`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns review with only body when all review comments are minimized", () => {
|
||||||
|
const reviewData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "review1",
|
||||||
|
databaseId: "300001",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
body: "Review body only",
|
||||||
|
state: "APPROVED",
|
||||||
|
submittedAt: "2023-01-01T00:00:00Z",
|
||||||
|
comments: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "comment1",
|
||||||
|
databaseId: "200001",
|
||||||
|
body: "Minimized comment 1",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/index.ts",
|
||||||
|
line: 42,
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comment2",
|
||||||
|
databaseId: "200002",
|
||||||
|
body: "Minimized comment 2",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/utils.ts",
|
||||||
|
line: 15,
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatReviewComments(reviewData);
|
||||||
|
expect(result).toBe(
|
||||||
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body only`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles multiple reviews with mixed minimized comments", () => {
|
||||||
|
const reviewData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "review1",
|
||||||
|
databaseId: "300001",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
body: "First review",
|
||||||
|
state: "APPROVED",
|
||||||
|
submittedAt: "2023-01-01T00:00:00Z",
|
||||||
|
comments: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "comment1",
|
||||||
|
databaseId: "200001",
|
||||||
|
body: "Good comment",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/index.ts",
|
||||||
|
line: 42,
|
||||||
|
isMinimized: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "review2",
|
||||||
|
databaseId: "300002",
|
||||||
|
author: { login: "reviewer2" },
|
||||||
|
body: "Second review",
|
||||||
|
state: "COMMENTED",
|
||||||
|
submittedAt: "2023-01-02T00:00:00Z",
|
||||||
|
comments: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "comment2",
|
||||||
|
databaseId: "200002",
|
||||||
|
body: "Spam comment",
|
||||||
|
author: { login: "reviewer2" },
|
||||||
|
createdAt: "2023-01-02T00:00:00Z",
|
||||||
|
path: "src/utils.ts",
|
||||||
|
line: 15,
|
||||||
|
isMinimized: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatReviewComments(reviewData);
|
||||||
|
expect(result).toBe(
|
||||||
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nFirst review\n [Comment on src/index.ts:42]: Good comment\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: COMMENTED\nSecond review`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("formatChangedFiles", () => {
|
describe("formatChangedFiles", () => {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type {
|
||||||
|
ParsedGitHubContext,
|
||||||
|
AutomationContext,
|
||||||
|
} from "../src/github/context";
|
||||||
import type {
|
import type {
|
||||||
IssuesEvent,
|
IssuesEvent,
|
||||||
IssueCommentEvent,
|
IssueCommentEvent,
|
||||||
@@ -38,7 +41,7 @@ export const createMockContext = (
|
|||||||
): ParsedGitHubContext => {
|
): ParsedGitHubContext => {
|
||||||
const baseContext: ParsedGitHubContext = {
|
const baseContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
eventName: "",
|
eventName: "issue_comment", // Default to a valid entity event
|
||||||
eventAction: "",
|
eventAction: "",
|
||||||
repository: defaultRepository,
|
repository: defaultRepository,
|
||||||
actor: "test-actor",
|
actor: "test-actor",
|
||||||
@@ -55,6 +58,22 @@ export const createMockContext = (
|
|||||||
return { ...baseContext, ...overrides };
|
return { ...baseContext, ...overrides };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMockAutomationContext = (
|
||||||
|
overrides: Partial<AutomationContext> = {},
|
||||||
|
): AutomationContext => {
|
||||||
|
const baseContext: AutomationContext = {
|
||||||
|
runId: "1234567890",
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
eventAction: undefined,
|
||||||
|
repository: defaultRepository,
|
||||||
|
actor: "test-actor",
|
||||||
|
payload: {} as any,
|
||||||
|
inputs: defaultInputs,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...baseContext, ...overrides };
|
||||||
|
};
|
||||||
|
|
||||||
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
eventName: "issues",
|
eventName: "issues",
|
||||||
|
|||||||
@@ -1,82 +1,59 @@
|
|||||||
import { describe, test, expect, beforeEach } from "bun:test";
|
import { describe, test, expect, beforeEach } from "bun:test";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import type { ParsedGitHubContext } from "../../src/github/context";
|
import type { GitHubContext } from "../../src/github/context";
|
||||||
import { createMockContext } from "../mockContext";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: ParsedGitHubContext;
|
let mockContext: GitHubContext;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockContext({
|
mockContext = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
isPR: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties and behavior", () => {
|
test("agent mode has correct properties", () => {
|
||||||
// Basic properties
|
|
||||||
expect(agentMode.name).toBe("agent");
|
expect(agentMode.name).toBe("agent");
|
||||||
expect(agentMode.description).toBe(
|
expect(agentMode.description).toBe(
|
||||||
"Automation mode that always runs without trigger checking",
|
"Automation mode for workflow_dispatch and schedule events",
|
||||||
);
|
);
|
||||||
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
||||||
|
|
||||||
// Tool methods return empty arrays
|
|
||||||
expect(agentMode.getAllowedTools()).toEqual([]);
|
expect(agentMode.getAllowedTools()).toEqual([]);
|
||||||
expect(agentMode.getDisallowedTools()).toEqual([]);
|
expect(agentMode.getDisallowedTools()).toEqual([]);
|
||||||
|
|
||||||
// Always triggers regardless of context
|
|
||||||
const contextWithoutTrigger = createMockContext({
|
|
||||||
eventName: "workflow_dispatch",
|
|
||||||
isPR: false,
|
|
||||||
inputs: {
|
|
||||||
...createMockContext().inputs,
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
},
|
|
||||||
payload: {} as any,
|
|
||||||
});
|
|
||||||
expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prepareContext includes all required data", () => {
|
test("prepareContext returns minimal data", () => {
|
||||||
const data = {
|
|
||||||
commentId: 789,
|
|
||||||
baseBranch: "develop",
|
|
||||||
claudeBranch: "claude/automated-task",
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = agentMode.prepareContext(mockContext, data);
|
|
||||||
|
|
||||||
expect(context.mode).toBe("agent");
|
|
||||||
expect(context.githubContext).toBe(mockContext);
|
|
||||||
expect(context.commentId).toBe(789);
|
|
||||||
expect(context.baseBranch).toBe("develop");
|
|
||||||
expect(context.claudeBranch).toBe("claude/automated-task");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("prepareContext works without data", () => {
|
|
||||||
const context = agentMode.prepareContext(mockContext);
|
const context = agentMode.prepareContext(mockContext);
|
||||||
|
|
||||||
expect(context.mode).toBe("agent");
|
expect(context.mode).toBe("agent");
|
||||||
expect(context.githubContext).toBe(mockContext);
|
expect(context.githubContext).toBe(mockContext);
|
||||||
expect(context.commentId).toBeUndefined();
|
// Agent mode doesn't use comment tracking or branch management
|
||||||
expect(context.baseBranch).toBeUndefined();
|
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
|
||||||
expect(context.claudeBranch).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode triggers for all event types", () => {
|
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
|
||||||
const events = [
|
// Should trigger for automation events
|
||||||
"push",
|
const workflowDispatchContext = createMockAutomationContext({
|
||||||
"schedule",
|
eventName: "workflow_dispatch",
|
||||||
"workflow_dispatch",
|
});
|
||||||
"repository_dispatch",
|
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
|
||||||
|
|
||||||
|
const scheduleContext = createMockAutomationContext({
|
||||||
|
eventName: "schedule",
|
||||||
|
});
|
||||||
|
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
|
||||||
|
|
||||||
|
// Should NOT trigger for entity events
|
||||||
|
const entityEvents = [
|
||||||
"issue_comment",
|
"issue_comment",
|
||||||
"pull_request",
|
"pull_request",
|
||||||
];
|
"pull_request_review",
|
||||||
|
"issues",
|
||||||
|
] as const;
|
||||||
|
|
||||||
events.forEach((eventName) => {
|
entityEvents.forEach((eventName) => {
|
||||||
const context = createMockContext({ eventName, isPR: false });
|
const context = createMockContext({ eventName });
|
||||||
expect(agentMode.shouldTrigger(context)).toBe(true);
|
expect(agentMode.shouldTrigger(context)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,23 +3,60 @@ import { getMode, isValidMode } from "../../src/modes/registry";
|
|||||||
import type { ModeName } from "../../src/modes/types";
|
import type { ModeName } from "../../src/modes/types";
|
||||||
import { tagMode } from "../../src/modes/tag";
|
import { tagMode } from "../../src/modes/tag";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
|
|
||||||
describe("Mode Registry", () => {
|
describe("Mode Registry", () => {
|
||||||
test("getMode returns tag mode by default", () => {
|
const mockContext = createMockContext({
|
||||||
const mode = getMode("tag");
|
eventName: "issue_comment",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockWorkflowDispatchContext = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockScheduleContext = createMockAutomationContext({
|
||||||
|
eventName: "schedule",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode returns tag mode for standard events", () => {
|
||||||
|
const mode = getMode("tag", mockContext);
|
||||||
expect(mode).toBe(tagMode);
|
expect(mode).toBe(tagMode);
|
||||||
expect(mode.name).toBe("tag");
|
expect(mode.name).toBe("tag");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMode returns agent mode", () => {
|
test("getMode returns agent mode", () => {
|
||||||
const mode = getMode("agent");
|
const mode = getMode("agent", mockContext);
|
||||||
|
expect(mode).toBe(agentMode);
|
||||||
|
expect(mode.name).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode throws error for tag mode with workflow_dispatch event", () => {
|
||||||
|
expect(() => getMode("tag", mockWorkflowDispatchContext)).toThrow(
|
||||||
|
"Tag mode cannot handle workflow_dispatch events. Use 'agent' mode for automation events.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode throws error for tag mode with schedule event", () => {
|
||||||
|
expect(() => getMode("tag", mockScheduleContext)).toThrow(
|
||||||
|
"Tag mode cannot handle schedule events. Use 'agent' mode for automation events.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode allows agent mode for workflow_dispatch event", () => {
|
||||||
|
const mode = getMode("agent", mockWorkflowDispatchContext);
|
||||||
|
expect(mode).toBe(agentMode);
|
||||||
|
expect(mode.name).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode allows agent mode for schedule event", () => {
|
||||||
|
const mode = getMode("agent", mockScheduleContext);
|
||||||
expect(mode).toBe(agentMode);
|
expect(mode).toBe(agentMode);
|
||||||
expect(mode.name).toBe("agent");
|
expect(mode.name).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMode throws error for invalid mode", () => {
|
test("getMode throws error for invalid mode", () => {
|
||||||
const invalidMode = "invalid" as unknown as ModeName;
|
const invalidMode = "invalid" as unknown as ModeName;
|
||||||
expect(() => getMode(invalidMode)).toThrow(
|
expect(() => getMode(invalidMode, mockContext)).toThrow(
|
||||||
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
|
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -299,15 +299,4 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
expect(result.allowedTools).toBe("Tool1,Tool2");
|
expect(result.allowedTools).toBe("Tool1,Tool2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error for unsupported event type", () => {
|
|
||||||
process.env = BASE_ENV;
|
|
||||||
const unsupportedContext = createMockContext({
|
|
||||||
eventName: "unsupported_event",
|
|
||||||
eventAction: "whatever",
|
|
||||||
});
|
|
||||||
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
|
|
||||||
"Unsupported event type: unsupported_event",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -474,17 +474,6 @@ describe("checkContainsTrigger", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("non-matching events", () => {
|
|
||||||
it("should return false for non-matching event type", () => {
|
|
||||||
const context = createMockContext({
|
|
||||||
eventName: "push",
|
|
||||||
eventAction: "created",
|
|
||||||
payload: {} as any,
|
|
||||||
});
|
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("escapeRegExp", () => {
|
describe("escapeRegExp", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user