mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-03-09 18:52:27 +08:00
refactor: simplify mode system by removing Mode interface and registry (#899)
Replace the over-engineered Mode interface/registry/detector pattern with straightforward inline logic. There are only 2 modes (tag and agent) and the complexity wasn't justified. - Delete Mode interface, registry, and prepare pass-through modules - Export prepareTagMode() and prepareAgentMode() as standalone functions - Inline trigger checking and mode dispatch in run.ts/prepare.ts - Change generatePrompt/createPrompt to take modeName string instead of Mode - Remove dead code (extractGitHubContext, unused detector helpers) - Update CLAUDE.md to reflect new architecture
This commit is contained in:
@@ -11,7 +11,7 @@ bun run format:check # Check formatting
|
||||
|
||||
## What This Is
|
||||
|
||||
A GitHub Action that lets Claude respond to `@claude` mentions on issues/PRs (tag mode) or run tasks via `prompt` input (agent mode). Mode is auto-detected: if `prompt` is provided, it's agent mode; if triggered by a comment/issue event with `@claude`, it's tag mode. See `src/modes/registry.ts`.
|
||||
A GitHub Action that lets Claude respond to `@claude` mentions on issues/PRs (tag mode) or run tasks via `prompt` input (agent mode). Mode is auto-detected: if `prompt` is provided, it's agent mode; if triggered by a comment/issue event with `@claude`, it's tag mode. See `src/modes/detector.ts`.
|
||||
|
||||
## How It Runs
|
||||
|
||||
@@ -23,9 +23,9 @@ Single entrypoint: `src/entrypoints/run.ts` orchestrates everything — prepare
|
||||
|
||||
**Auth priority**: `github_token` input (user-provided) > GitHub App OIDC token (default). The `claude_code_oauth_token` and `anthropic_api_key` are for the Claude API, not GitHub. Token setup lives in `src/github/token.ts`.
|
||||
|
||||
**Mode lifecycle**: Modes implement `shouldTrigger()` → `prepare()` → `prepareContext()` → `getSystemPrompt()`. The registry in `src/modes/registry.ts` picks the mode based on event type and inputs. To add a new mode, implement the `Mode` type from `src/modes/types.ts` and register it.
|
||||
**Mode lifecycle**: `detectMode()` in `src/modes/detector.ts` picks the mode name ("tag" or "agent"). Trigger checking and prepare dispatch are inlined in `run.ts`: tag mode calls `prepareTagMode()` from `src/modes/tag/`, agent mode calls `prepareAgentMode()` from `src/modes/agent/`.
|
||||
|
||||
**Prompt construction**: `src/prepare/` builds the prompt by fetching GitHub data (`src/github/data/fetcher.ts`), formatting it as markdown (`src/github/data/formatter.ts`), and writing it to a temp file. The prompt includes issue/PR body, comments, diff, and CI status. This is the most important part of the action — it's what Claude sees.
|
||||
**Prompt construction**: Tag mode's `prepareTagMode()` builds the prompt by fetching GitHub data (`src/github/data/fetcher.ts`), formatting it as markdown (`src/github/data/formatter.ts`), and writing it to a temp file via `createPrompt()`. Agent mode writes the user's prompt directly. The prompt includes issue/PR body, comments, diff, and CI status. This is the most important part of the action — it's what Claude sees.
|
||||
|
||||
## Things That Will Bite You
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import type { CommonFields, PreparedContext, EventData } from "./types";
|
||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import type { Mode, ModeContext } from "../modes/types";
|
||||
import { extractUserRequest } from "../utils/extract-user-request";
|
||||
export type { CommonFields, PreparedContext } from "./types";
|
||||
|
||||
@@ -458,9 +457,31 @@ export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
mode: Mode,
|
||||
modeName: "tag" | "agent",
|
||||
): string {
|
||||
return mode.generatePrompt(context, githubData, useCommitSigning);
|
||||
if (modeName === "agent") {
|
||||
return context.prompt || `Repository: ${context.repository}`;
|
||||
}
|
||||
|
||||
// Tag mode
|
||||
const defaultPrompt = generateDefaultPrompt(
|
||||
context,
|
||||
githubData,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
if (context.githubContext?.inputs?.prompt) {
|
||||
return (
|
||||
defaultPrompt +
|
||||
`
|
||||
|
||||
<custom_instructions>
|
||||
${context.githubContext.inputs.prompt}
|
||||
</custom_instructions>`
|
||||
);
|
||||
}
|
||||
|
||||
return defaultPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -901,28 +922,20 @@ function extractUserRequestFromContext(
|
||||
}
|
||||
|
||||
export async function createPrompt(
|
||||
mode: Mode,
|
||||
modeContext: ModeContext,
|
||||
commentId: number,
|
||||
baseBranch: string | undefined,
|
||||
claudeBranch: string | undefined,
|
||||
githubData: FetchDataResult,
|
||||
context: ParsedGitHubContext,
|
||||
) {
|
||||
try {
|
||||
// Prepare the context for prompt generation
|
||||
let claudeCommentId: string = "";
|
||||
if (mode.name === "tag") {
|
||||
if (!modeContext.commentId) {
|
||||
throw new Error(
|
||||
`${mode.name} mode requires a comment ID for prompt generation`,
|
||||
);
|
||||
}
|
||||
claudeCommentId = modeContext.commentId.toString();
|
||||
}
|
||||
const claudeCommentId = commentId.toString();
|
||||
|
||||
const preparedContext = prepareContext(
|
||||
context,
|
||||
claudeCommentId,
|
||||
modeContext.baseBranch,
|
||||
modeContext.claudeBranch,
|
||||
baseBranch,
|
||||
claudeBranch,
|
||||
);
|
||||
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
@@ -934,7 +947,7 @@ export async function createPrompt(
|
||||
preparedContext,
|
||||
githubData,
|
||||
context.inputs.useCommitSigning,
|
||||
mode,
|
||||
"tag",
|
||||
);
|
||||
|
||||
// Log the final prompt to console
|
||||
@@ -967,19 +980,12 @@ export async function createPrompt(
|
||||
// Set allowed tools
|
||||
const hasActionsReadPermission = false;
|
||||
|
||||
// Get mode-specific tools
|
||||
const modeAllowedTools = mode.getAllowedTools();
|
||||
const modeDisallowedTools = mode.getDisallowedTools();
|
||||
|
||||
const allAllowedTools = buildAllowedToolsString(
|
||||
modeAllowedTools,
|
||||
[],
|
||||
hasActionsReadPermission,
|
||||
context.inputs.useCommitSigning,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString(
|
||||
modeDisallowedTools,
|
||||
modeAllowedTools,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString([], []);
|
||||
|
||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
||||
|
||||
@@ -10,8 +10,10 @@ import { setupGitHubToken } from "../github/token";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { prepare } from "../prepare";
|
||||
import { detectMode } from "../modes/detector";
|
||||
import { prepareTagMode } from "../modes/tag";
|
||||
import { prepareAgentMode } from "../modes/agent";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
|
||||
async function run() {
|
||||
@@ -22,7 +24,10 @@ async function run() {
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Auto-detect mode based on context
|
||||
const mode = getMode(context);
|
||||
const modeName = detectMode(context);
|
||||
console.log(
|
||||
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
// Setup GitHub token
|
||||
const githubToken = await setupGitHubToken();
|
||||
@@ -46,10 +51,13 @@ async function run() {
|
||||
}
|
||||
|
||||
// Check trigger conditions
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
const containsTrigger =
|
||||
modeName === "tag"
|
||||
? isEntityContext(context) && checkContainsTrigger(context)
|
||||
: !!context.inputs?.prompt;
|
||||
|
||||
// Debug logging
|
||||
console.log(`Mode: ${mode.name}`);
|
||||
console.log(`Mode: ${modeName}`);
|
||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||
console.log(`Trigger result: ${containsTrigger}`);
|
||||
|
||||
@@ -63,31 +71,20 @@ async function run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Use the new modular prepare function
|
||||
const result = await prepare({
|
||||
context,
|
||||
octokit,
|
||||
mode,
|
||||
githubToken,
|
||||
});
|
||||
// Run prepare
|
||||
console.log(
|
||||
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
if (modeName === "tag") {
|
||||
await prepareTagMode({ context, octokit, githubToken });
|
||||
} else {
|
||||
await prepareAgentMode({ context, octokit, githubToken });
|
||||
}
|
||||
|
||||
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output
|
||||
|
||||
// Expose the GitHub token (Claude App token) as an output
|
||||
core.setOutput("github_token", githubToken);
|
||||
|
||||
// Step 6: Get system prompt from mode if available
|
||||
if (mode.getSystemPrompt) {
|
||||
const modeContext = mode.prepareContext(context, {
|
||||
commentId: result.commentId,
|
||||
baseBranch: result.branchInfo.baseBranch,
|
||||
claudeBranch: result.branchInfo.claudeBranch,
|
||||
});
|
||||
const systemPrompt = mode.getSystemPrompt(modeContext);
|
||||
if (systemPrompt) {
|
||||
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||
|
||||
@@ -17,8 +17,10 @@ import { createOctokit } from "../github/api/client";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { prepare } from "../prepare";
|
||||
import { detectMode } from "../modes/detector";
|
||||
import { prepareTagMode } from "../modes/tag";
|
||||
import { prepareAgentMode } from "../modes/agent";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
import { updateCommentLink } from "./update-comment-link";
|
||||
import { formatTurnsFromData } from "./format-turns";
|
||||
@@ -138,7 +140,10 @@ async function run() {
|
||||
// Phase 1: Prepare
|
||||
const actionInputsPresent = collectActionInputsPresence();
|
||||
context = parseGitHubContext();
|
||||
const mode = getMode(context);
|
||||
const modeName = detectMode(context);
|
||||
console.log(
|
||||
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
try {
|
||||
githubToken = await setupGitHubToken();
|
||||
@@ -173,8 +178,11 @@ async function run() {
|
||||
}
|
||||
|
||||
// Check trigger conditions
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
console.log(`Mode: ${mode.name}`);
|
||||
const containsTrigger =
|
||||
modeName === "tag"
|
||||
? isEntityContext(context) && checkContainsTrigger(context)
|
||||
: !!context.inputs?.prompt;
|
||||
console.log(`Mode: ${modeName}`);
|
||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||
console.log(`Trigger result: ${containsTrigger}`);
|
||||
|
||||
@@ -185,31 +193,19 @@ async function run() {
|
||||
}
|
||||
|
||||
// Run prepare
|
||||
const prepareResult = await prepare({
|
||||
context,
|
||||
octokit,
|
||||
mode,
|
||||
githubToken,
|
||||
});
|
||||
console.log(
|
||||
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
const prepareResult =
|
||||
modeName === "tag"
|
||||
? await prepareTagMode({ context, octokit, githubToken })
|
||||
: await prepareAgentMode({ context, octokit, githubToken });
|
||||
|
||||
commentId = prepareResult.commentId;
|
||||
claudeBranch = prepareResult.branchInfo.claudeBranch;
|
||||
baseBranch = prepareResult.branchInfo.baseBranch;
|
||||
prepareCompleted = true;
|
||||
|
||||
// Set system prompt if available
|
||||
if (mode.getSystemPrompt) {
|
||||
const modeContext = mode.prepareContext(context, {
|
||||
commentId: prepareResult.commentId,
|
||||
baseBranch: prepareResult.branchInfo.baseBranch,
|
||||
claudeBranch: prepareResult.branchInfo.claudeBranch,
|
||||
});
|
||||
const systemPrompt = mode.getSystemPrompt(modeContext);
|
||||
if (systemPrompt) {
|
||||
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Install Claude Code CLI
|
||||
await installClaudeCode();
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import * as core from "@actions/core";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
import type { PreparedContext } from "../../create-prompt/types";
|
||||
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||
import { parseAllowedTools } from "./parse-tools";
|
||||
import {
|
||||
@@ -10,211 +7,128 @@ import {
|
||||
} from "../../github/operations/git-config";
|
||||
import { checkHumanActor } from "../../github/validation/actor";
|
||||
import type { GitHubContext } from "../../github/context";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
import type { Octokits } from "../../github/api/client";
|
||||
|
||||
/**
|
||||
* Extract GitHub context as environment variables for agent mode
|
||||
*/
|
||||
function extractGitHubContext(context: GitHubContext): Record<string, string> {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
// Basic repository info
|
||||
envVars.GITHUB_REPOSITORY = context.repository.full_name;
|
||||
envVars.GITHUB_TRIGGER_ACTOR = context.actor;
|
||||
envVars.GITHUB_EVENT_NAME = context.eventName;
|
||||
|
||||
// Entity-specific context (PR/issue numbers, branches, etc.)
|
||||
if (isEntityContext(context)) {
|
||||
if (context.isPR) {
|
||||
envVars.GITHUB_PR_NUMBER = String(context.entityNumber);
|
||||
|
||||
// Extract branch info from payload if available
|
||||
if (
|
||||
context.payload &&
|
||||
"pull_request" in context.payload &&
|
||||
context.payload.pull_request
|
||||
) {
|
||||
envVars.GITHUB_BASE_REF = context.payload.pull_request.base?.ref || "";
|
||||
envVars.GITHUB_HEAD_REF = context.payload.pull_request.head?.ref || "";
|
||||
}
|
||||
} else {
|
||||
envVars.GITHUB_ISSUE_NUMBER = String(context.entityNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent mode implementation.
|
||||
* Prepares the agent mode execution context.
|
||||
*
|
||||
* This mode runs whenever an explicit prompt is provided in the workflow configuration.
|
||||
* Agent mode runs whenever an explicit prompt is provided in the workflow configuration.
|
||||
* It bypasses the standard @claude mention checking and comment tracking used by tag mode,
|
||||
* providing direct access to Claude Code for automation workflows.
|
||||
*/
|
||||
export const agentMode: Mode = {
|
||||
name: "agent",
|
||||
description: "Direct automation mode for explicit prompts",
|
||||
export async function prepareAgentMode({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
githubToken: string;
|
||||
}) {
|
||||
// Check if actor is human (prevents bot-triggered loops)
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
shouldTrigger(context) {
|
||||
// Only trigger when an explicit prompt is provided
|
||||
return !!context.inputs?.prompt;
|
||||
},
|
||||
// Configure git authentication for agent mode (same as tag mode)
|
||||
// SSH signing takes precedence if provided
|
||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||
const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
|
||||
|
||||
prepareContext(context) {
|
||||
// Agent mode doesn't use comment tracking or branch management
|
||||
return {
|
||||
mode: "agent",
|
||||
githubContext: context,
|
||||
if (useSshSigning) {
|
||||
// Setup SSH signing for commits
|
||||
await setupSshSigning(context.inputs.sshSigningKey);
|
||||
|
||||
// Still configure git auth for push operations (user/email and remote URL)
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
// Continue anyway - git operations may still work with default config
|
||||
}
|
||||
} else if (!useApiCommitSigning) {
|
||||
// Use bot_id and bot_name from inputs directly
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
},
|
||||
|
||||
getAllowedTools() {
|
||||
return [];
|
||||
},
|
||||
try {
|
||||
// Use the shared git configuration function
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
// Continue anyway - git operations may still work with default config
|
||||
}
|
||||
}
|
||||
|
||||
getDisallowedTools() {
|
||||
return [];
|
||||
},
|
||||
// Create prompt directory
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
shouldCreateTrackingComment() {
|
||||
return false;
|
||||
},
|
||||
// Write the prompt file - use the user's prompt directly
|
||||
const promptContent =
|
||||
context.inputs.prompt ||
|
||||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||
|
||||
async prepare({
|
||||
context,
|
||||
octokit,
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||
promptContent,
|
||||
);
|
||||
|
||||
// Parse allowed tools from user's claude_args
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
const allowedTools = parseAllowedTools(userClaudeArgs);
|
||||
|
||||
// Check for branch info from environment variables (useful for auto-fix workflows)
|
||||
const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
|
||||
const baseBranch =
|
||||
process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
|
||||
|
||||
// Detect current branch from GitHub environment
|
||||
const currentBranch =
|
||||
claudeBranch ||
|
||||
process.env.GITHUB_HEAD_REF ||
|
||||
process.env.GITHUB_REF_NAME ||
|
||||
"main";
|
||||
|
||||
// Get our GitHub MCP servers config
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
// Check if actor is human (prevents bot-triggered loops)
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: currentBranch,
|
||||
baseBranch: baseBranch,
|
||||
claudeCommentId: undefined, // No tracking comment in agent mode
|
||||
allowedTools,
|
||||
mode: "agent",
|
||||
context,
|
||||
});
|
||||
|
||||
// Configure git authentication for agent mode (same as tag mode)
|
||||
// SSH signing takes precedence if provided
|
||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||
const useApiCommitSigning =
|
||||
context.inputs.useCommitSigning && !useSshSigning;
|
||||
// Build final claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
if (useSshSigning) {
|
||||
// Setup SSH signing for commits
|
||||
await setupSshSigning(context.inputs.sshSigningKey);
|
||||
// Add our GitHub servers config if we have any
|
||||
const ourConfig = JSON.parse(ourMcpConfig);
|
||||
if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
}
|
||||
|
||||
// Still configure git auth for push operations (user/email and remote URL)
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
// Continue anyway - git operations may still work with default config
|
||||
}
|
||||
} else if (!useApiCommitSigning) {
|
||||
// Use bot_id and bot_name from inputs directly
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
|
||||
|
||||
try {
|
||||
// Use the shared git configuration function
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
// Continue anyway - git operations may still work with default config
|
||||
}
|
||||
}
|
||||
|
||||
// Create prompt directory
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Write the prompt file - use the user's prompt directly
|
||||
const promptContent =
|
||||
context.inputs.prompt ||
|
||||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||
promptContent,
|
||||
);
|
||||
|
||||
// Parse allowed tools from user's claude_args
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
const allowedTools = parseAllowedTools(userClaudeArgs);
|
||||
|
||||
// Check for branch info from environment variables (useful for auto-fix workflows)
|
||||
const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
|
||||
const baseBranch =
|
||||
process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
|
||||
|
||||
// Detect current branch from GitHub environment
|
||||
const currentBranch =
|
||||
claudeBranch ||
|
||||
process.env.GITHUB_HEAD_REF ||
|
||||
process.env.GITHUB_REF_NAME ||
|
||||
"main";
|
||||
|
||||
// Get our GitHub MCP servers config
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: currentBranch,
|
||||
return {
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: baseBranch,
|
||||
claudeCommentId: undefined, // No tracking comment in agent mode
|
||||
allowedTools,
|
||||
mode: "agent",
|
||||
context,
|
||||
});
|
||||
|
||||
// Build final claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
// Add our GitHub servers config if we have any
|
||||
const ourConfig = JSON.parse(ourMcpConfig);
|
||||
if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
}
|
||||
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
|
||||
|
||||
return {
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: baseBranch,
|
||||
currentBranch: baseBranch, // Use base branch as current when creating new branch
|
||||
claudeBranch: claudeBranch,
|
||||
},
|
||||
mcpConfig: ourMcpConfig,
|
||||
claudeArgs,
|
||||
};
|
||||
},
|
||||
|
||||
generatePrompt(context: PreparedContext): string {
|
||||
// Inject GitHub context as environment variables
|
||||
if (context.githubContext) {
|
||||
const envVars = extractGitHubContext(context.githubContext);
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
core.exportVariable(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Agent mode uses prompt field
|
||||
if (context.prompt) {
|
||||
return context.prompt;
|
||||
}
|
||||
|
||||
// Minimal fallback - repository is a string in PreparedContext
|
||||
return `Repository: ${context.repository}`;
|
||||
},
|
||||
|
||||
getSystemPrompt() {
|
||||
// Agent mode doesn't need additional system prompts
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
currentBranch: baseBranch, // Use base branch as current when creating new branch
|
||||
claudeBranch: claudeBranch,
|
||||
},
|
||||
mcpConfig: ourMcpConfig,
|
||||
claudeArgs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,17 +80,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
|
||||
return "agent";
|
||||
}
|
||||
|
||||
export function getModeDescription(mode: AutoDetectedMode): string {
|
||||
switch (mode) {
|
||||
case "tag":
|
||||
return "Interactive mode triggered by @claude mentions";
|
||||
case "agent":
|
||||
return "Direct automation mode for explicit prompts";
|
||||
default:
|
||||
return "Unknown mode";
|
||||
}
|
||||
}
|
||||
|
||||
function validateTrackProgressEvent(context: GitHubContext): void {
|
||||
// track_progress is only valid for pull_request and issue events
|
||||
const validEvents = [
|
||||
@@ -123,21 +112,3 @@ function validateTrackProgressEvent(context: GitHubContext): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldUseTrackingComment(mode: AutoDetectedMode): boolean {
|
||||
return mode === "tag";
|
||||
}
|
||||
|
||||
export function getDefaultPromptForMode(
|
||||
mode: AutoDetectedMode,
|
||||
context: GitHubContext,
|
||||
): string | undefined {
|
||||
switch (mode) {
|
||||
case "tag":
|
||||
return undefined;
|
||||
case "agent":
|
||||
return context.inputs?.prompt;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Mode Registry for claude-code-action v1.0
|
||||
*
|
||||
* This module provides access to all available execution modes and handles
|
||||
* automatic mode detection based on GitHub event types.
|
||||
*/
|
||||
|
||||
import type { Mode, ModeName } from "./types";
|
||||
import { tagMode } from "./tag";
|
||||
import { agentMode } from "./agent";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { detectMode, type AutoDetectedMode } from "./detector";
|
||||
|
||||
export const VALID_MODES = ["tag", "agent"] as const;
|
||||
|
||||
/**
|
||||
* All available modes in v1.0
|
||||
*/
|
||||
const modes = {
|
||||
tag: tagMode,
|
||||
agent: agentMode,
|
||||
} as const satisfies Record<AutoDetectedMode, Mode>;
|
||||
|
||||
/**
|
||||
* Automatically detects and retrieves the appropriate mode based on the GitHub context.
|
||||
* In v1.0, modes are auto-selected based on event type.
|
||||
* @param context The GitHub context
|
||||
* @returns The appropriate mode for the context
|
||||
*/
|
||||
export function getMode(context: GitHubContext): Mode {
|
||||
const modeName = detectMode(context);
|
||||
console.log(
|
||||
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
const mode = modes[modeName];
|
||||
if (!mode) {
|
||||
throw new Error(
|
||||
`Mode '${modeName}' not found. This should not happen. Please report this issue.`,
|
||||
);
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid mode name.
|
||||
* @param name The string to check
|
||||
* @returns True if the name is a valid mode name
|
||||
*/
|
||||
export function isValidMode(name: string): name is ModeName {
|
||||
const validModes = ["tag", "agent"];
|
||||
return validModes.includes(name);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
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";
|
||||
@@ -14,241 +12,177 @@ import {
|
||||
extractOriginalTitle,
|
||||
extractOriginalBody,
|
||||
} from "../../github/data/fetcher";
|
||||
import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
|
||||
import { createPrompt } from "../../create-prompt";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
import type { PreparedContext } from "../../create-prompt/types";
|
||||
import type { FetchDataResult } from "../../github/data/fetcher";
|
||||
import type { GitHubContext } from "../../github/context";
|
||||
import type { Octokits } from "../../github/api/client";
|
||||
import { parseAllowedTools } from "../agent/parse-tools";
|
||||
|
||||
/**
|
||||
* Tag mode implementation.
|
||||
* Prepares the tag mode execution context.
|
||||
*
|
||||
* The traditional implementation mode that responds to @claude mentions,
|
||||
* issue assignments, or labels. Creates tracking comments showing progress
|
||||
* and has full implementation capabilities.
|
||||
* Tag mode responds to @claude mentions, issue assignments, or labels.
|
||||
* Creates tracking comments showing progress and has full implementation capabilities.
|
||||
*/
|
||||
export const tagMode: Mode = {
|
||||
name: "tag",
|
||||
description: "Traditional implementation mode triggered by @claude mentions",
|
||||
export async function prepareTagMode({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
githubToken: string;
|
||||
}) {
|
||||
// Tag mode only handles entity-based events
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("Tag mode requires entity context");
|
||||
}
|
||||
|
||||
shouldTrigger(context) {
|
||||
// Tag mode only handles entity events
|
||||
if (!isEntityContext(context)) {
|
||||
return false;
|
||||
}
|
||||
return checkContainsTrigger(context);
|
||||
},
|
||||
// Check if actor is human
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
prepareContext(context, data) {
|
||||
return {
|
||||
mode: "tag",
|
||||
githubContext: context,
|
||||
commentId: data?.commentId,
|
||||
baseBranch: data?.baseBranch,
|
||||
claudeBranch: data?.claudeBranch,
|
||||
// Create initial tracking comment
|
||||
const commentData = await createInitialComment(octokit.rest, context);
|
||||
const commentId = commentData.id;
|
||||
|
||||
const triggerTime = extractTriggerTimestamp(context);
|
||||
const originalTitle = extractOriginalTitle(context);
|
||||
const originalBody = extractOriginalBody(context);
|
||||
|
||||
const githubData = await fetchGitHubData({
|
||||
octokits: octokit,
|
||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||
prNumber: context.entityNumber.toString(),
|
||||
isPR: context.isPR,
|
||||
triggerUsername: context.actor,
|
||||
triggerTime,
|
||||
originalTitle,
|
||||
originalBody,
|
||||
includeCommentsByActor: context.inputs.includeCommentsByActor,
|
||||
excludeCommentsByActor: context.inputs.excludeCommentsByActor,
|
||||
});
|
||||
|
||||
// Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
|
||||
// Configure git authentication
|
||||
// SSH signing takes precedence if provided
|
||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||
const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
|
||||
|
||||
if (useSshSigning) {
|
||||
// Setup SSH signing for commits
|
||||
await setupSshSigning(context.inputs.sshSigningKey);
|
||||
|
||||
// Still configure git auth for push operations (user/email and remote URL)
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
} else if (!useApiCommitSigning) {
|
||||
// Use bot_id and bot_name from inputs directly
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
},
|
||||
|
||||
getAllowedTools() {
|
||||
return [];
|
||||
},
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getDisallowedTools() {
|
||||
return [];
|
||||
},
|
||||
|
||||
shouldCreateTrackingComment() {
|
||||
return true;
|
||||
},
|
||||
|
||||
async prepare({
|
||||
// Create prompt file
|
||||
await createPrompt(
|
||||
commentId,
|
||||
branchInfo.baseBranch,
|
||||
branchInfo.claudeBranch,
|
||||
githubData,
|
||||
context,
|
||||
octokit,
|
||||
);
|
||||
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter((tool) =>
|
||||
tool.startsWith("mcp__github_"),
|
||||
);
|
||||
|
||||
// Build claude_args for tag mode with required tools
|
||||
// Tag mode REQUIRES these tools to function properly
|
||||
const tagModeTools = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
"mcp__github_ci__get_ci_status",
|
||||
"mcp__github_ci__get_workflow_run_details",
|
||||
"mcp__github_ci__download_job_log",
|
||||
...userAllowedMCPTools,
|
||||
];
|
||||
|
||||
// Add git commands when using git CLI (no API commit signing, or SSH signing)
|
||||
// SSH signing still uses git CLI, just with signing enabled
|
||||
if (!useApiCommitSigning) {
|
||||
tagModeTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git rm:*)",
|
||||
);
|
||||
} else {
|
||||
// When using API commit signing, use MCP file ops tools
|
||||
tagModeTools.push(
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__delete_files",
|
||||
);
|
||||
}
|
||||
|
||||
// Get our GitHub MCP servers configuration
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
// Tag mode only handles entity-based events
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("Tag mode requires entity context");
|
||||
}
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeCommentId: commentId.toString(),
|
||||
allowedTools: Array.from(new Set(tagModeTools)),
|
||||
mode: "tag",
|
||||
context,
|
||||
});
|
||||
|
||||
// Check if actor is human
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
// Build complete claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
// Create initial tracking comment
|
||||
const commentData = await createInitialComment(octokit.rest, context);
|
||||
const commentId = commentData.id;
|
||||
// Add our GitHub servers config
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
|
||||
const triggerTime = extractTriggerTimestamp(context);
|
||||
const originalTitle = extractOriginalTitle(context);
|
||||
const originalBody = extractOriginalBody(context);
|
||||
// Add required tools for tag mode
|
||||
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
||||
|
||||
const githubData = await fetchGitHubData({
|
||||
octokits: octokit,
|
||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||
prNumber: context.entityNumber.toString(),
|
||||
isPR: context.isPR,
|
||||
triggerUsername: context.actor,
|
||||
triggerTime,
|
||||
originalTitle,
|
||||
originalBody,
|
||||
includeCommentsByActor: context.inputs.includeCommentsByActor,
|
||||
excludeCommentsByActor: context.inputs.excludeCommentsByActor,
|
||||
});
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
if (userClaudeArgs) {
|
||||
claudeArgs += ` ${userClaudeArgs}`;
|
||||
}
|
||||
|
||||
// Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
|
||||
// Configure git authentication
|
||||
// SSH signing takes precedence if provided
|
||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||
const useApiCommitSigning =
|
||||
context.inputs.useCommitSigning && !useSshSigning;
|
||||
|
||||
if (useSshSigning) {
|
||||
// Setup SSH signing for commits
|
||||
await setupSshSigning(context.inputs.sshSigningKey);
|
||||
|
||||
// Still configure git auth for push operations (user/email and remote URL)
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
} else if (!useApiCommitSigning) {
|
||||
// Use bot_id and bot_name from inputs directly
|
||||
const user = {
|
||||
login: context.inputs.botName,
|
||||
id: parseInt(context.inputs.botId),
|
||||
};
|
||||
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, 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);
|
||||
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter(
|
||||
(tool) => tool.startsWith("mcp__github_"),
|
||||
);
|
||||
|
||||
// Build claude_args for tag mode with required tools
|
||||
// Tag mode REQUIRES these tools to function properly
|
||||
const tagModeTools = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
"mcp__github_ci__get_ci_status",
|
||||
"mcp__github_ci__get_workflow_run_details",
|
||||
"mcp__github_ci__download_job_log",
|
||||
...userAllowedMCPTools,
|
||||
];
|
||||
|
||||
// Add git commands when using git CLI (no API commit signing, or SSH signing)
|
||||
// SSH signing still uses git CLI, just with signing enabled
|
||||
if (!useApiCommitSigning) {
|
||||
tagModeTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git rm:*)",
|
||||
);
|
||||
} else {
|
||||
// When using API commit signing, use MCP file ops tools
|
||||
tagModeTools.push(
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__delete_files",
|
||||
);
|
||||
}
|
||||
|
||||
// Get our GitHub MCP servers configuration
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeCommentId: commentId.toString(),
|
||||
allowedTools: Array.from(new Set(tagModeTools)),
|
||||
mode: "tag",
|
||||
context,
|
||||
});
|
||||
|
||||
// Build complete claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
// Add our GitHub servers config
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
|
||||
// Add required tools for tag mode
|
||||
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
||||
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
if (userClaudeArgs) {
|
||||
claudeArgs += ` ${userClaudeArgs}`;
|
||||
}
|
||||
|
||||
return {
|
||||
commentId,
|
||||
branchInfo,
|
||||
mcpConfig: ourMcpConfig,
|
||||
claudeArgs: claudeArgs.trim(),
|
||||
};
|
||||
},
|
||||
|
||||
generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
const defaultPrompt = generateDefaultPrompt(
|
||||
context,
|
||||
githubData,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
// If a custom prompt is provided, inject it into the tag mode prompt
|
||||
if (context.githubContext?.inputs?.prompt) {
|
||||
return (
|
||||
defaultPrompt +
|
||||
`
|
||||
|
||||
<custom_instructions>
|
||||
${context.githubContext.inputs.prompt}
|
||||
</custom_instructions>`
|
||||
);
|
||||
}
|
||||
|
||||
return defaultPrompt;
|
||||
},
|
||||
|
||||
getSystemPrompt() {
|
||||
// Tag mode doesn't need additional system prompts
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
return {
|
||||
commentId,
|
||||
branchInfo,
|
||||
mcpConfig: ourMcpConfig,
|
||||
claudeArgs: claudeArgs.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import type { PreparedContext } from "../create-prompt/types";
|
||||
import type { FetchDataResult } from "../github/data/fetcher";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
|
||||
export type ModeName = "tag" | "agent";
|
||||
|
||||
export type ModeContext = {
|
||||
mode: ModeName;
|
||||
githubContext: GitHubContext;
|
||||
commentId?: number;
|
||||
baseBranch?: string;
|
||||
claudeBranch?: string;
|
||||
};
|
||||
|
||||
export type ModeData = {
|
||||
commentId?: number;
|
||||
baseBranch?: string;
|
||||
claudeBranch?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mode interface for claude-code-action execution modes.
|
||||
* Each mode defines its own behavior for trigger detection, prompt generation,
|
||||
* and tracking comment creation.
|
||||
*
|
||||
* Current modes include:
|
||||
* - 'tag': Interactive mode triggered by @claude mentions
|
||||
* - 'agent': Direct automation mode triggered by explicit prompts
|
||||
*/
|
||||
export type Mode = {
|
||||
name: ModeName;
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Determines if this mode should trigger based on the GitHub context
|
||||
*/
|
||||
shouldTrigger(context: GitHubContext): boolean;
|
||||
|
||||
/**
|
||||
* Prepares the mode context with any additional data needed for prompt generation
|
||||
*/
|
||||
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
|
||||
|
||||
/**
|
||||
* Returns the list of tools that should be allowed for this mode
|
||||
*/
|
||||
getAllowedTools(): string[];
|
||||
|
||||
/**
|
||||
* Returns the list of tools that should be disallowed for this mode
|
||||
*/
|
||||
getDisallowedTools(): string[];
|
||||
|
||||
/**
|
||||
* Determines if this mode should create a tracking comment
|
||||
*/
|
||||
shouldCreateTrackingComment(): boolean;
|
||||
|
||||
/**
|
||||
* Generates the prompt for this mode.
|
||||
* @returns The complete prompt string
|
||||
*/
|
||||
generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* Returns an optional system prompt to append to Claude's base system prompt.
|
||||
* This allows modes to add mode-specific instructions.
|
||||
* @returns The system prompt string or undefined if no additional prompt is needed
|
||||
*/
|
||||
getSystemPrompt?(context: ModeContext): string | undefined;
|
||||
};
|
||||
|
||||
// Define types for mode prepare method
|
||||
export type ModeOptions = {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
githubToken: string;
|
||||
};
|
||||
|
||||
export type ModeResult = {
|
||||
commentId?: number;
|
||||
branchInfo: {
|
||||
baseBranch: string;
|
||||
claudeBranch?: string;
|
||||
currentBranch: string;
|
||||
};
|
||||
mcpConfig: string;
|
||||
claudeArgs: string;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
claudeArgs: string;
|
||||
};
|
||||
|
||||
export type PrepareOptions = {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
mode: Mode;
|
||||
githubToken: string;
|
||||
};
|
||||
@@ -3,60 +3,13 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
generatePrompt,
|
||||
generateDefaultPrompt,
|
||||
getEventTypeAndContext,
|
||||
buildAllowedToolsString,
|
||||
buildDisallowedToolsString,
|
||||
} from "../src/create-prompt";
|
||||
import type { PreparedContext } from "../src/create-prompt";
|
||||
import type { Mode } from "../src/modes/types";
|
||||
|
||||
describe("generatePrompt", () => {
|
||||
// Create a mock tag mode that uses the default prompt
|
||||
const mockTagMode: Mode = {
|
||||
name: "tag",
|
||||
description: "Tag mode",
|
||||
shouldTrigger: () => true,
|
||||
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
|
||||
getAllowedTools: () => [],
|
||||
getDisallowedTools: () => [],
|
||||
shouldCreateTrackingComment: () => true,
|
||||
generatePrompt: (context, githubData, useCommitSigning) =>
|
||||
generateDefaultPrompt(context, githubData, useCommitSigning),
|
||||
prepare: async () => ({
|
||||
commentId: 123,
|
||||
branchInfo: {
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: "{}",
|
||||
claudeArgs: "",
|
||||
}),
|
||||
};
|
||||
|
||||
// Create a mock agent mode that passes through prompts
|
||||
const mockAgentMode: Mode = {
|
||||
name: "agent",
|
||||
description: "Agent mode",
|
||||
shouldTrigger: () => true,
|
||||
prepareContext: (context) => ({ mode: "agent", githubContext: context }),
|
||||
getAllowedTools: () => [],
|
||||
getDisallowedTools: () => [],
|
||||
shouldCreateTrackingComment: () => false,
|
||||
generatePrompt: (context) => context.prompt || "",
|
||||
prepare: async () => ({
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: "{}",
|
||||
claudeArgs: "",
|
||||
}),
|
||||
};
|
||||
|
||||
const mockGitHubData = {
|
||||
contextData: {
|
||||
title: "Test PR",
|
||||
@@ -181,12 +134,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
||||
@@ -214,12 +162,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -245,12 +188,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -278,12 +216,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -310,12 +243,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -341,12 +269,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -370,12 +293,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Verify prompt generates successfully without custom instructions
|
||||
expect(prompt).toContain("@claude please fix this");
|
||||
@@ -400,7 +318,7 @@ describe("generatePrompt", () => {
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockAgentMode,
|
||||
"agent",
|
||||
);
|
||||
|
||||
// Agent mode: Prompt is passed through as-is
|
||||
@@ -441,7 +359,7 @@ describe("generatePrompt", () => {
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockAgentMode,
|
||||
"agent",
|
||||
);
|
||||
|
||||
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
|
||||
@@ -490,7 +408,7 @@ describe("generatePrompt", () => {
|
||||
envVars,
|
||||
issueGitHubData,
|
||||
false,
|
||||
mockAgentMode,
|
||||
"agent",
|
||||
);
|
||||
|
||||
// Agent mode: Prompt is passed through as-is
|
||||
@@ -515,7 +433,7 @@ describe("generatePrompt", () => {
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockAgentMode,
|
||||
"agent",
|
||||
);
|
||||
|
||||
// Agent mode: No substitution - passed as-is
|
||||
@@ -539,12 +457,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
@@ -567,12 +480,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||
// With commit signing disabled, co-author info appears in git commit instructions
|
||||
@@ -594,12 +502,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain PR-specific instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
@@ -630,12 +533,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain Issue-specific instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -674,12 +572,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain the actual branch name with timestamp
|
||||
expect(prompt).toContain(
|
||||
@@ -709,12 +602,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain branch-specific instructions like issues
|
||||
expect(prompt).toContain(
|
||||
@@ -752,12 +640,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain open PR instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
@@ -788,12 +671,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -821,12 +699,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -854,12 +727,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -883,12 +751,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should have git command instructions
|
||||
expect(prompt).toContain("Use git commands via the Bash tool");
|
||||
@@ -917,12 +780,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = await generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
true,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = await generatePrompt(envVars, mockGitHubData, true, "tag");
|
||||
|
||||
// Should have commit signing tool instructions
|
||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||
|
||||
@@ -7,22 +7,17 @@ import {
|
||||
spyOn,
|
||||
mock,
|
||||
} from "bun:test";
|
||||
import { agentMode } from "../../src/modes/agent";
|
||||
import type { GitHubContext } from "../../src/github/context";
|
||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||
import { prepareAgentMode } from "../../src/modes/agent";
|
||||
import { createMockAutomationContext } from "../mockContext";
|
||||
import * as core from "@actions/core";
|
||||
import * as gitConfig from "../../src/github/operations/git-config";
|
||||
|
||||
describe("Agent Mode", () => {
|
||||
let mockContext: GitHubContext;
|
||||
let exportVariableSpy: any;
|
||||
let setOutputSpy: any;
|
||||
let configureGitAuthSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
||||
() => {},
|
||||
);
|
||||
@@ -45,84 +40,11 @@ describe("Agent Mode", () => {
|
||||
configureGitAuthSpy?.mockRestore();
|
||||
});
|
||||
|
||||
test("agent mode has correct properties", () => {
|
||||
expect(agentMode.name).toBe("agent");
|
||||
expect(agentMode.description).toBe(
|
||||
"Direct automation mode for explicit prompts",
|
||||
);
|
||||
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
||||
expect(agentMode.getAllowedTools()).toEqual([]);
|
||||
expect(agentMode.getDisallowedTools()).toEqual([]);
|
||||
test("prepareAgentMode is exported as a function", () => {
|
||||
expect(typeof prepareAgentMode).toBe("function");
|
||||
});
|
||||
|
||||
test("prepareContext returns minimal data", () => {
|
||||
const context = agentMode.prepareContext(mockContext);
|
||||
|
||||
expect(context.mode).toBe("agent");
|
||||
expect(context.githubContext).toBe(mockContext);
|
||||
// Agent mode doesn't use comment tracking or branch management
|
||||
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
|
||||
});
|
||||
|
||||
test("agent mode only triggers when prompt is provided", () => {
|
||||
// Should NOT trigger for automation events without prompt
|
||||
const workflowDispatchContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(false);
|
||||
|
||||
const scheduleContext = createMockAutomationContext({
|
||||
eventName: "schedule",
|
||||
});
|
||||
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
|
||||
|
||||
const repositoryDispatchContext = createMockAutomationContext({
|
||||
eventName: "repository_dispatch",
|
||||
});
|
||||
expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
|
||||
|
||||
// Should NOT trigger for entity events without prompt
|
||||
const entityEvents = [
|
||||
"issue_comment",
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"issues",
|
||||
] as const;
|
||||
|
||||
entityEvents.forEach((eventName) => {
|
||||
const contextNoPrompt = createMockContext({ eventName });
|
||||
expect(agentMode.shouldTrigger(contextNoPrompt)).toBe(false);
|
||||
});
|
||||
|
||||
// Should trigger for ANY event when prompt is provided
|
||||
const allEvents = [
|
||||
"workflow_dispatch",
|
||||
"repository_dispatch",
|
||||
"schedule",
|
||||
"issue_comment",
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"issues",
|
||||
] as const;
|
||||
|
||||
allEvents.forEach((eventName) => {
|
||||
const contextWithPrompt =
|
||||
eventName === "workflow_dispatch" ||
|
||||
eventName === "repository_dispatch" ||
|
||||
eventName === "schedule"
|
||||
? createMockAutomationContext({
|
||||
eventName,
|
||||
inputs: { prompt: "Do something" },
|
||||
})
|
||||
: createMockContext({
|
||||
eventName,
|
||||
inputs: { prompt: "Do something" },
|
||||
});
|
||||
expect(agentMode.shouldTrigger(contextWithPrompt)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("prepare method passes through claude_args", async () => {
|
||||
test("prepare passes through claude_args", async () => {
|
||||
// Clear any previous calls before this test
|
||||
exportVariableSpy.mockClear();
|
||||
setOutputSpy.mockClear();
|
||||
@@ -156,7 +78,7 @@ describe("Agent Mode", () => {
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const result = await agentMode.prepare({
|
||||
const result = await prepareAgentMode({
|
||||
context: contextWithCustomArgs,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
@@ -186,7 +108,7 @@ describe("Agent Mode", () => {
|
||||
process.env.GITHUB_REF_NAME = originalRefName;
|
||||
});
|
||||
|
||||
test("prepare method rejects bot actors without allowed_bots", async () => {
|
||||
test("prepare rejects bot actors without allowed_bots", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
@@ -206,7 +128,7 @@ describe("Agent Mode", () => {
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
prepareAgentMode({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
@@ -216,7 +138,7 @@ describe("Agent Mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("prepare method allows bot actors when in allowed_bots list", async () => {
|
||||
test("prepare allows bot actors when in allowed_bots list", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
@@ -237,7 +159,7 @@ describe("Agent Mode", () => {
|
||||
|
||||
// Should not throw - bot is in allowed list
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
prepareAgentMode({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
@@ -245,7 +167,7 @@ describe("Agent Mode", () => {
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test("prepare method creates prompt file with correct content", async () => {
|
||||
test("prepare creates prompt file with correct content", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
@@ -268,7 +190,7 @@ describe("Agent Mode", () => {
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const result = await agentMode.prepare({
|
||||
const result = await prepareAgentMode({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getMode, isValidMode } from "../../src/modes/registry";
|
||||
import { agentMode } from "../../src/modes/agent";
|
||||
import { tagMode } from "../../src/modes/tag";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockAutomationContext,
|
||||
mockRepositoryDispatchContext,
|
||||
} from "../mockContext";
|
||||
|
||||
describe("Mode Registry", () => {
|
||||
const mockContext = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
payload: {
|
||||
action: "created",
|
||||
comment: {
|
||||
body: "Test comment without trigger",
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
const mockWorkflowDispatchContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
|
||||
const mockScheduleContext = createMockAutomationContext({
|
||||
eventName: "schedule",
|
||||
});
|
||||
|
||||
test("getMode auto-detects agent mode for issue_comment without trigger", () => {
|
||||
const mode = getMode(mockContext);
|
||||
// Agent mode is the default when no trigger is found
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode auto-detects agent mode for workflow_dispatch", () => {
|
||||
const mode = getMode(mockWorkflowDispatchContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
// Removed test - explicit mode override no longer supported in v1.0
|
||||
|
||||
test("getMode auto-detects agent for workflow_dispatch", () => {
|
||||
const mode = getMode(mockWorkflowDispatchContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode auto-detects agent for schedule event", () => {
|
||||
const mode = getMode(mockScheduleContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode auto-detects agent for repository_dispatch event", () => {
|
||||
const mode = getMode(mockRepositoryDispatchContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode auto-detects agent for repository_dispatch with client_payload", () => {
|
||||
const contextWithPayload = createMockAutomationContext({
|
||||
eventName: "repository_dispatch",
|
||||
payload: {
|
||||
action: "trigger-analysis",
|
||||
client_payload: {
|
||||
source: "external-system",
|
||||
metadata: { priority: "high" },
|
||||
},
|
||||
repository: {
|
||||
name: "test-repo",
|
||||
owner: { login: "test-owner" },
|
||||
},
|
||||
sender: { login: "automation-user" },
|
||||
},
|
||||
});
|
||||
|
||||
const mode = getMode(contextWithPayload);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
// Removed test - legacy mode names no longer supported in v1.0
|
||||
|
||||
test("getMode auto-detects agent mode for PR opened", () => {
|
||||
const prContext = createMockContext({
|
||||
eventName: "pull_request",
|
||||
payload: { action: "opened" } as any,
|
||||
isPR: true,
|
||||
});
|
||||
const mode = getMode(prContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode uses agent mode when prompt is provided, even with @claude mention", () => {
|
||||
const contextWithPrompt = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
payload: {
|
||||
action: "created",
|
||||
comment: {
|
||||
body: "@claude please help",
|
||||
},
|
||||
} as any,
|
||||
inputs: {
|
||||
prompt: "/review",
|
||||
} as any,
|
||||
});
|
||||
const mode = getMode(contextWithPrompt);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode uses tag mode for @claude mention without prompt", () => {
|
||||
// Ensure PROMPT env var is not set (clean up from previous tests)
|
||||
const originalPrompt = process.env.PROMPT;
|
||||
delete process.env.PROMPT;
|
||||
|
||||
const contextWithMention = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
payload: {
|
||||
action: "created",
|
||||
comment: {
|
||||
body: "@claude please help",
|
||||
},
|
||||
} as any,
|
||||
inputs: {
|
||||
triggerPhrase: "@claude",
|
||||
prompt: "",
|
||||
} as any,
|
||||
});
|
||||
const mode = getMode(contextWithMention);
|
||||
expect(mode).toBe(tagMode);
|
||||
expect(mode.name).toBe("tag");
|
||||
|
||||
// Restore original value if it existed
|
||||
if (originalPrompt !== undefined) {
|
||||
process.env.PROMPT = originalPrompt;
|
||||
}
|
||||
});
|
||||
|
||||
// Removed test - explicit mode override no longer supported in v1.0
|
||||
|
||||
test("isValidMode returns true for all valid modes", () => {
|
||||
expect(isValidMode("tag")).toBe(true);
|
||||
expect(isValidMode("agent")).toBe(true);
|
||||
});
|
||||
|
||||
test("isValidMode returns false for invalid mode", () => {
|
||||
expect(isValidMode("invalid")).toBe(false);
|
||||
expect(isValidMode("review")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,92 +1,8 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test";
|
||||
import { tagMode } from "../../src/modes/tag";
|
||||
import type { ParsedGitHubContext } from "../../src/github/context";
|
||||
import type { IssueCommentEvent } from "@octokit/webhooks-types";
|
||||
import { createMockContext } from "../mockContext";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { prepareTagMode } from "../../src/modes/tag";
|
||||
|
||||
describe("Tag Mode", () => {
|
||||
let mockContext: ParsedGitHubContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
isPR: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("tag mode has correct properties", () => {
|
||||
expect(tagMode.name).toBe("tag");
|
||||
expect(tagMode.description).toBe(
|
||||
"Traditional implementation mode triggered by @claude mentions",
|
||||
);
|
||||
expect(tagMode.shouldCreateTrackingComment()).toBe(true);
|
||||
});
|
||||
|
||||
test("shouldTrigger delegates to checkContainsTrigger", () => {
|
||||
const contextWithTrigger = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
isPR: false,
|
||||
inputs: {
|
||||
...createMockContext().inputs,
|
||||
triggerPhrase: "@claude",
|
||||
},
|
||||
payload: {
|
||||
comment: {
|
||||
body: "Hey @claude, can you help?",
|
||||
},
|
||||
} as IssueCommentEvent,
|
||||
});
|
||||
|
||||
expect(tagMode.shouldTrigger(contextWithTrigger)).toBe(true);
|
||||
|
||||
const contextWithoutTrigger = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
isPR: false,
|
||||
inputs: {
|
||||
...createMockContext().inputs,
|
||||
triggerPhrase: "@claude",
|
||||
},
|
||||
payload: {
|
||||
comment: {
|
||||
body: "This is just a regular comment",
|
||||
},
|
||||
} as IssueCommentEvent,
|
||||
});
|
||||
|
||||
expect(tagMode.shouldTrigger(contextWithoutTrigger)).toBe(false);
|
||||
});
|
||||
|
||||
test("prepareContext includes all required data", () => {
|
||||
const data = {
|
||||
commentId: 123,
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/fix-bug",
|
||||
};
|
||||
|
||||
const context = tagMode.prepareContext(mockContext, data);
|
||||
|
||||
expect(context.mode).toBe("tag");
|
||||
expect(context.githubContext).toBe(mockContext);
|
||||
expect(context.commentId).toBe(123);
|
||||
expect(context.baseBranch).toBe("main");
|
||||
expect(context.claudeBranch).toBe("claude/fix-bug");
|
||||
});
|
||||
|
||||
test("prepareContext works without data", () => {
|
||||
const context = tagMode.prepareContext(mockContext);
|
||||
|
||||
expect(context.mode).toBe("tag");
|
||||
expect(context.githubContext).toBe(mockContext);
|
||||
expect(context.commentId).toBeUndefined();
|
||||
expect(context.baseBranch).toBeUndefined();
|
||||
expect(context.claudeBranch).toBeUndefined();
|
||||
});
|
||||
|
||||
test("getAllowedTools returns empty array", () => {
|
||||
expect(tagMode.getAllowedTools()).toEqual([]);
|
||||
});
|
||||
|
||||
test("getDisallowedTools returns empty array", () => {
|
||||
expect(tagMode.getDisallowedTools()).toEqual([]);
|
||||
test("prepareTagMode is exported as a function", () => {
|
||||
expect(typeof prepareTagMode).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,10 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
getEventTypeAndContext,
|
||||
generatePrompt,
|
||||
generateDefaultPrompt,
|
||||
} from "../src/create-prompt";
|
||||
import { getEventTypeAndContext, generatePrompt } from "../src/create-prompt";
|
||||
import type { PreparedContext } from "../src/create-prompt";
|
||||
import type { Mode } from "../src/modes/types";
|
||||
|
||||
describe("pull_request_target event support", () => {
|
||||
// Mock tag mode for testing
|
||||
const mockTagMode: Mode = {
|
||||
name: "tag",
|
||||
description: "Tag mode",
|
||||
shouldTrigger: () => true,
|
||||
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
|
||||
getAllowedTools: () => [],
|
||||
getDisallowedTools: () => [],
|
||||
shouldCreateTrackingComment: () => true,
|
||||
generatePrompt: (context, githubData, useCommitSigning) =>
|
||||
generateDefaultPrompt(context, githubData, useCommitSigning),
|
||||
prepare: async () => ({
|
||||
commentId: 123,
|
||||
branchInfo: {
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: "{}",
|
||||
claudeArgs: "",
|
||||
}),
|
||||
};
|
||||
|
||||
const mockGitHubData = {
|
||||
contextData: {
|
||||
title: "External PR via pull_request_target",
|
||||
@@ -126,12 +98,7 @@ describe("pull_request_target event support", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should contain pull request event type and metadata
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
@@ -165,12 +132,7 @@ describe("pull_request_target event support", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should include git commands for non-commit-signing mode
|
||||
expect(prompt).toContain("git push");
|
||||
@@ -196,7 +158,7 @@ describe("pull_request_target event support", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, true, "tag");
|
||||
|
||||
// Should include commit signing tools
|
||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||
@@ -246,13 +208,13 @@ describe("pull_request_target event support", () => {
|
||||
pullRequestContext,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
"tag",
|
||||
);
|
||||
const pullRequestTargetPrompt = generatePrompt(
|
||||
pullRequestTargetContext,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
"tag",
|
||||
);
|
||||
|
||||
// Both should have the same event type and structure
|
||||
@@ -293,37 +255,7 @@ describe("pull_request_target event support", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// Use agent mode which passes through the prompt as-is
|
||||
const mockAgentMode: Mode = {
|
||||
name: "agent",
|
||||
description: "Agent mode",
|
||||
shouldTrigger: () => true,
|
||||
prepareContext: (context) => ({
|
||||
mode: "agent",
|
||||
githubContext: context,
|
||||
}),
|
||||
getAllowedTools: () => [],
|
||||
getDisallowedTools: () => [],
|
||||
shouldCreateTrackingComment: () => true,
|
||||
generatePrompt: (context) => context.prompt || "default prompt",
|
||||
prepare: async () => ({
|
||||
commentId: 123,
|
||||
branchInfo: {
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: "{}",
|
||||
claudeArgs: "",
|
||||
}),
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockAgentMode,
|
||||
);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, "agent");
|
||||
|
||||
expect(prompt).toBe(
|
||||
"Review this pull_request_target PR for security issues",
|
||||
@@ -343,12 +275,7 @@ describe("pull_request_target event support", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(
|
||||
envVars,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||
|
||||
// Should generate default prompt structure
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
@@ -418,7 +345,7 @@ describe("pull_request_target event support", () => {
|
||||
|
||||
// Should not throw when generating prompt
|
||||
expect(() => {
|
||||
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
|
||||
generatePrompt(minimalContext, mockGitHubData, false, "tag");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -476,13 +403,13 @@ describe("pull_request_target event support", () => {
|
||||
internalPR,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
"tag",
|
||||
);
|
||||
const externalPrompt = generatePrompt(
|
||||
externalPR,
|
||||
mockGitHubData,
|
||||
false,
|
||||
mockTagMode,
|
||||
"tag",
|
||||
);
|
||||
|
||||
// Should have same tool access patterns
|
||||
|
||||
Reference in New Issue
Block a user