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:
parent
f09dc9a6a3
commit
7057f3318b
@ -11,7 +11,7 @@ bun run format:check # Check formatting
|
|||||||
|
|
||||||
## What This Is
|
## 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
|
## 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`.
|
**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
|
## Things That Will Bite You
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import {
|
|||||||
import type { ParsedGitHubContext } from "../github/context";
|
import type { ParsedGitHubContext } from "../github/context";
|
||||||
import type { CommonFields, PreparedContext, EventData } from "./types";
|
import type { CommonFields, PreparedContext, EventData } from "./types";
|
||||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||||
import type { Mode, ModeContext } from "../modes/types";
|
|
||||||
import { extractUserRequest } from "../utils/extract-user-request";
|
import { extractUserRequest } from "../utils/extract-user-request";
|
||||||
export type { CommonFields, PreparedContext } from "./types";
|
export type { CommonFields, PreparedContext } from "./types";
|
||||||
|
|
||||||
@ -458,9 +457,31 @@ export function generatePrompt(
|
|||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
useCommitSigning: boolean,
|
useCommitSigning: boolean,
|
||||||
mode: Mode,
|
modeName: "tag" | "agent",
|
||||||
): string {
|
): 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(
|
export async function createPrompt(
|
||||||
mode: Mode,
|
commentId: number,
|
||||||
modeContext: ModeContext,
|
baseBranch: string | undefined,
|
||||||
|
claudeBranch: string | undefined,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Prepare the context for prompt generation
|
const claudeCommentId = commentId.toString();
|
||||||
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 preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
context,
|
context,
|
||||||
claudeCommentId,
|
claudeCommentId,
|
||||||
modeContext.baseBranch,
|
baseBranch,
|
||||||
modeContext.claudeBranch,
|
claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
@ -934,7 +947,7 @@ export async function createPrompt(
|
|||||||
preparedContext,
|
preparedContext,
|
||||||
githubData,
|
githubData,
|
||||||
context.inputs.useCommitSigning,
|
context.inputs.useCommitSigning,
|
||||||
mode,
|
"tag",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log the final prompt to console
|
// Log the final prompt to console
|
||||||
@ -967,19 +980,12 @@ export async function createPrompt(
|
|||||||
// Set allowed tools
|
// Set allowed tools
|
||||||
const hasActionsReadPermission = false;
|
const hasActionsReadPermission = false;
|
||||||
|
|
||||||
// Get mode-specific tools
|
|
||||||
const modeAllowedTools = mode.getAllowedTools();
|
|
||||||
const modeDisallowedTools = mode.getDisallowedTools();
|
|
||||||
|
|
||||||
const allAllowedTools = buildAllowedToolsString(
|
const allAllowedTools = buildAllowedToolsString(
|
||||||
modeAllowedTools,
|
[],
|
||||||
hasActionsReadPermission,
|
hasActionsReadPermission,
|
||||||
context.inputs.useCommitSigning,
|
context.inputs.useCommitSigning,
|
||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString([], []);
|
||||||
modeDisallowedTools,
|
|
||||||
modeAllowedTools,
|
|
||||||
);
|
|
||||||
|
|
||||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||||
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import { setupGitHubToken } from "../github/token";
|
|||||||
import { checkWritePermissions } from "../github/validation/permissions";
|
import { checkWritePermissions } from "../github/validation/permissions";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||||
import { getMode } from "../modes/registry";
|
import { detectMode } from "../modes/detector";
|
||||||
import { prepare } from "../prepare";
|
import { prepareTagMode } from "../modes/tag";
|
||||||
|
import { prepareAgentMode } from "../modes/agent";
|
||||||
|
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||||
import { collectActionInputsPresence } from "./collect-inputs";
|
import { collectActionInputsPresence } from "./collect-inputs";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@ -22,7 +24,10 @@ async function run() {
|
|||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
|
|
||||||
// Auto-detect mode based on context
|
// 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
|
// Setup GitHub token
|
||||||
const githubToken = await setupGitHubToken();
|
const githubToken = await setupGitHubToken();
|
||||||
@ -46,10 +51,13 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check trigger conditions
|
// Check trigger conditions
|
||||||
const containsTrigger = mode.shouldTrigger(context);
|
const containsTrigger =
|
||||||
|
modeName === "tag"
|
||||||
|
? isEntityContext(context) && checkContainsTrigger(context)
|
||||||
|
: !!context.inputs?.prompt;
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
console.log(`Mode: ${mode.name}`);
|
console.log(`Mode: ${modeName}`);
|
||||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||||
console.log(`Trigger result: ${containsTrigger}`);
|
console.log(`Trigger result: ${containsTrigger}`);
|
||||||
|
|
||||||
@ -63,31 +71,20 @@ async function run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Use the new modular prepare function
|
// Run prepare
|
||||||
const result = await prepare({
|
console.log(
|
||||||
context,
|
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||||
octokit,
|
);
|
||||||
mode,
|
if (modeName === "tag") {
|
||||||
githubToken,
|
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
|
// 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
|
// Expose the GitHub token (Claude App token) as an output
|
||||||
core.setOutput("github_token", githubToken);
|
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) {
|
} 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}`);
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import { createOctokit } from "../github/api/client";
|
|||||||
import type { Octokits } from "../github/api/client";
|
import type { Octokits } from "../github/api/client";
|
||||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||||
import type { GitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
import { getMode } from "../modes/registry";
|
import { detectMode } from "../modes/detector";
|
||||||
import { prepare } from "../prepare";
|
import { prepareTagMode } from "../modes/tag";
|
||||||
|
import { prepareAgentMode } from "../modes/agent";
|
||||||
|
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||||
import { collectActionInputsPresence } from "./collect-inputs";
|
import { collectActionInputsPresence } from "./collect-inputs";
|
||||||
import { updateCommentLink } from "./update-comment-link";
|
import { updateCommentLink } from "./update-comment-link";
|
||||||
import { formatTurnsFromData } from "./format-turns";
|
import { formatTurnsFromData } from "./format-turns";
|
||||||
@ -138,7 +140,10 @@ async function run() {
|
|||||||
// Phase 1: Prepare
|
// Phase 1: Prepare
|
||||||
const actionInputsPresent = collectActionInputsPresence();
|
const actionInputsPresent = collectActionInputsPresence();
|
||||||
context = parseGitHubContext();
|
context = parseGitHubContext();
|
||||||
const mode = getMode(context);
|
const modeName = detectMode(context);
|
||||||
|
console.log(
|
||||||
|
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
githubToken = await setupGitHubToken();
|
githubToken = await setupGitHubToken();
|
||||||
@ -173,8 +178,11 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check trigger conditions
|
// Check trigger conditions
|
||||||
const containsTrigger = mode.shouldTrigger(context);
|
const containsTrigger =
|
||||||
console.log(`Mode: ${mode.name}`);
|
modeName === "tag"
|
||||||
|
? isEntityContext(context) && checkContainsTrigger(context)
|
||||||
|
: !!context.inputs?.prompt;
|
||||||
|
console.log(`Mode: ${modeName}`);
|
||||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||||
console.log(`Trigger result: ${containsTrigger}`);
|
console.log(`Trigger result: ${containsTrigger}`);
|
||||||
|
|
||||||
@ -185,31 +193,19 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run prepare
|
// Run prepare
|
||||||
const prepareResult = await prepare({
|
console.log(
|
||||||
context,
|
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||||
octokit,
|
);
|
||||||
mode,
|
const prepareResult =
|
||||||
githubToken,
|
modeName === "tag"
|
||||||
});
|
? await prepareTagMode({ context, octokit, githubToken })
|
||||||
|
: await prepareAgentMode({ context, octokit, githubToken });
|
||||||
|
|
||||||
commentId = prepareResult.commentId;
|
commentId = prepareResult.commentId;
|
||||||
claudeBranch = prepareResult.branchInfo.claudeBranch;
|
claudeBranch = prepareResult.branchInfo.claudeBranch;
|
||||||
baseBranch = prepareResult.branchInfo.baseBranch;
|
baseBranch = prepareResult.branchInfo.baseBranch;
|
||||||
prepareCompleted = true;
|
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
|
// Phase 2: Install Claude Code CLI
|
||||||
await installClaudeCode();
|
await installClaudeCode();
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { mkdir, writeFile } from "fs/promises";
|
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 { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||||
import { parseAllowedTools } from "./parse-tools";
|
import { parseAllowedTools } from "./parse-tools";
|
||||||
import {
|
import {
|
||||||
@ -10,211 +7,128 @@ import {
|
|||||||
} from "../../github/operations/git-config";
|
} from "../../github/operations/git-config";
|
||||||
import { checkHumanActor } from "../../github/validation/actor";
|
import { checkHumanActor } from "../../github/validation/actor";
|
||||||
import type { GitHubContext } from "../../github/context";
|
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
|
* Prepares the agent mode execution context.
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*
|
*
|
||||||
* 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,
|
* It bypasses the standard @claude mention checking and comment tracking used by tag mode,
|
||||||
* providing direct access to Claude Code for automation workflows.
|
* providing direct access to Claude Code for automation workflows.
|
||||||
*/
|
*/
|
||||||
export const agentMode: Mode = {
|
export async function prepareAgentMode({
|
||||||
name: "agent",
|
context,
|
||||||
description: "Direct automation mode for explicit prompts",
|
octokit,
|
||||||
|
githubToken,
|
||||||
|
}: {
|
||||||
|
context: GitHubContext;
|
||||||
|
octokit: Octokits;
|
||||||
|
githubToken: string;
|
||||||
|
}) {
|
||||||
|
// Check if actor is human (prevents bot-triggered loops)
|
||||||
|
await checkHumanActor(octokit.rest, context);
|
||||||
|
|
||||||
shouldTrigger(context) {
|
// Configure git authentication for agent mode (same as tag mode)
|
||||||
// Only trigger when an explicit prompt is provided
|
// SSH signing takes precedence if provided
|
||||||
return !!context.inputs?.prompt;
|
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||||
},
|
const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
|
||||||
|
|
||||||
prepareContext(context) {
|
if (useSshSigning) {
|
||||||
// Agent mode doesn't use comment tracking or branch management
|
// Setup SSH signing for commits
|
||||||
return {
|
await setupSshSigning(context.inputs.sshSigningKey);
|
||||||
mode: "agent",
|
|
||||||
githubContext: context,
|
// 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() {
|
try {
|
||||||
return [];
|
// 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() {
|
// Create prompt directory
|
||||||
return [];
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
},
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
shouldCreateTrackingComment() {
|
// Write the prompt file - use the user's prompt directly
|
||||||
return false;
|
const promptContent =
|
||||||
},
|
context.inputs.prompt ||
|
||||||
|
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||||
|
|
||||||
async prepare({
|
await writeFile(
|
||||||
context,
|
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||||
octokit,
|
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,
|
githubToken,
|
||||||
}: ModeOptions): Promise<ModeResult> {
|
owner: context.repository.owner,
|
||||||
// Check if actor is human (prevents bot-triggered loops)
|
repo: context.repository.repo,
|
||||||
await checkHumanActor(octokit.rest, context);
|
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)
|
// Build final claude_args with multiple --mcp-config flags
|
||||||
// SSH signing takes precedence if provided
|
let claudeArgs = "";
|
||||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
|
||||||
const useApiCommitSigning =
|
|
||||||
context.inputs.useCommitSigning && !useSshSigning;
|
|
||||||
|
|
||||||
if (useSshSigning) {
|
// Add our GitHub servers config if we have any
|
||||||
// Setup SSH signing for commits
|
const ourConfig = JSON.parse(ourMcpConfig);
|
||||||
await setupSshSigning(context.inputs.sshSigningKey);
|
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)
|
// Append user's claude_args (which may have more --mcp-config flags)
|
||||||
const user = {
|
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
|
||||||
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),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
return {
|
||||||
// Use the shared git configuration function
|
commentId: undefined,
|
||||||
await configureGitAuth(githubToken, context, user);
|
branchInfo: {
|
||||||
} 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,
|
|
||||||
baseBranch: baseBranch,
|
baseBranch: baseBranch,
|
||||||
claudeCommentId: undefined, // No tracking comment in agent mode
|
currentBranch: baseBranch, // Use base branch as current when creating new branch
|
||||||
allowedTools,
|
claudeBranch: claudeBranch,
|
||||||
mode: "agent",
|
},
|
||||||
context,
|
mcpConfig: ourMcpConfig,
|
||||||
});
|
claudeArgs,
|
||||||
|
};
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -80,17 +80,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
|
|||||||
return "agent";
|
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 {
|
function validateTrackProgressEvent(context: GitHubContext): void {
|
||||||
// track_progress is only valid for pull_request and issue events
|
// track_progress is only valid for pull_request and issue events
|
||||||
const validEvents = [
|
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 { checkHumanActor } from "../../github/validation/actor";
|
||||||
import { createInitialComment } from "../../github/operations/comments/create-initial";
|
import { createInitialComment } from "../../github/operations/comments/create-initial";
|
||||||
import { setupBranch } from "../../github/operations/branch";
|
import { setupBranch } from "../../github/operations/branch";
|
||||||
@ -14,241 +12,177 @@ import {
|
|||||||
extractOriginalTitle,
|
extractOriginalTitle,
|
||||||
extractOriginalBody,
|
extractOriginalBody,
|
||||||
} from "../../github/data/fetcher";
|
} from "../../github/data/fetcher";
|
||||||
import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
|
import { createPrompt } from "../../create-prompt";
|
||||||
import { isEntityContext } from "../../github/context";
|
import { isEntityContext } from "../../github/context";
|
||||||
import type { PreparedContext } from "../../create-prompt/types";
|
import type { GitHubContext } from "../../github/context";
|
||||||
import type { FetchDataResult } from "../../github/data/fetcher";
|
import type { Octokits } from "../../github/api/client";
|
||||||
import { parseAllowedTools } from "../agent/parse-tools";
|
import { parseAllowedTools } from "../agent/parse-tools";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag mode implementation.
|
* Prepares the tag mode execution context.
|
||||||
*
|
*
|
||||||
* The traditional implementation mode that responds to @claude mentions,
|
* Tag mode responds to @claude mentions, issue assignments, or labels.
|
||||||
* issue assignments, or labels. Creates tracking comments showing progress
|
* Creates tracking comments showing progress and has full implementation capabilities.
|
||||||
* and has full implementation capabilities.
|
|
||||||
*/
|
*/
|
||||||
export const tagMode: Mode = {
|
export async function prepareTagMode({
|
||||||
name: "tag",
|
context,
|
||||||
description: "Traditional implementation mode triggered by @claude mentions",
|
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) {
|
// Check if actor is human
|
||||||
// Tag mode only handles entity events
|
await checkHumanActor(octokit.rest, context);
|
||||||
if (!isEntityContext(context)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return checkContainsTrigger(context);
|
|
||||||
},
|
|
||||||
|
|
||||||
prepareContext(context, data) {
|
// Create initial tracking comment
|
||||||
return {
|
const commentData = await createInitialComment(octokit.rest, context);
|
||||||
mode: "tag",
|
const commentId = commentData.id;
|
||||||
githubContext: context,
|
|
||||||
commentId: data?.commentId,
|
const triggerTime = extractTriggerTimestamp(context);
|
||||||
baseBranch: data?.baseBranch,
|
const originalTitle = extractOriginalTitle(context);
|
||||||
claudeBranch: data?.claudeBranch,
|
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() {
|
try {
|
||||||
return [];
|
await configureGitAuth(githubToken, context, user);
|
||||||
},
|
} catch (error) {
|
||||||
|
console.error("Failed to configure git authentication:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getDisallowedTools() {
|
// Create prompt file
|
||||||
return [];
|
await createPrompt(
|
||||||
},
|
commentId,
|
||||||
|
branchInfo.baseBranch,
|
||||||
shouldCreateTrackingComment() {
|
branchInfo.claudeBranch,
|
||||||
return true;
|
githubData,
|
||||||
},
|
|
||||||
|
|
||||||
async prepare({
|
|
||||||
context,
|
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,
|
githubToken,
|
||||||
}: ModeOptions): Promise<ModeResult> {
|
owner: context.repository.owner,
|
||||||
// Tag mode only handles entity-based events
|
repo: context.repository.repo,
|
||||||
if (!isEntityContext(context)) {
|
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||||
throw new Error("Tag mode requires entity context");
|
baseBranch: branchInfo.baseBranch,
|
||||||
}
|
claudeCommentId: commentId.toString(),
|
||||||
|
allowedTools: Array.from(new Set(tagModeTools)),
|
||||||
|
mode: "tag",
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if actor is human
|
// Build complete claude_args with multiple --mcp-config flags
|
||||||
await checkHumanActor(octokit.rest, context);
|
let claudeArgs = "";
|
||||||
|
|
||||||
// Create initial tracking comment
|
// Add our GitHub servers config
|
||||||
const commentData = await createInitialComment(octokit.rest, context);
|
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||||
const commentId = commentData.id;
|
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||||
|
|
||||||
const triggerTime = extractTriggerTimestamp(context);
|
// Add required tools for tag mode
|
||||||
const originalTitle = extractOriginalTitle(context);
|
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
||||||
const originalBody = extractOriginalBody(context);
|
|
||||||
|
|
||||||
const githubData = await fetchGitHubData({
|
// Append user's claude_args (which may have more --mcp-config flags)
|
||||||
octokits: octokit,
|
if (userClaudeArgs) {
|
||||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
claudeArgs += ` ${userClaudeArgs}`;
|
||||||
prNumber: context.entityNumber.toString(),
|
}
|
||||||
isPR: context.isPR,
|
|
||||||
triggerUsername: context.actor,
|
|
||||||
triggerTime,
|
|
||||||
originalTitle,
|
|
||||||
originalBody,
|
|
||||||
includeCommentsByActor: context.inputs.includeCommentsByActor,
|
|
||||||
excludeCommentsByActor: context.inputs.excludeCommentsByActor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup branch
|
return {
|
||||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
commentId,
|
||||||
|
branchInfo,
|
||||||
// Configure git authentication
|
mcpConfig: ourMcpConfig,
|
||||||
// SSH signing takes precedence if provided
|
claudeArgs: claudeArgs.trim(),
|
||||||
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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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 { describe, test, expect } from "bun:test";
|
||||||
import {
|
import {
|
||||||
generatePrompt,
|
generatePrompt,
|
||||||
generateDefaultPrompt,
|
|
||||||
getEventTypeAndContext,
|
getEventTypeAndContext,
|
||||||
buildAllowedToolsString,
|
buildAllowedToolsString,
|
||||||
buildDisallowedToolsString,
|
buildDisallowedToolsString,
|
||||||
} from "../src/create-prompt";
|
} from "../src/create-prompt";
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
import type { PreparedContext } from "../src/create-prompt";
|
||||||
import type { Mode } from "../src/modes/types";
|
|
||||||
|
|
||||||
describe("generatePrompt", () => {
|
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 = {
|
const mockGitHubData = {
|
||||||
contextData: {
|
contextData: {
|
||||||
title: "Test PR",
|
title: "Test PR",
|
||||||
@ -181,12 +134,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||||
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
||||||
@ -214,12 +162,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
||||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||||
@ -245,12 +188,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -278,12 +216,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -310,12 +243,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -341,12 +269,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||||
@ -370,12 +293,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify prompt generates successfully without custom instructions
|
// Verify prompt generates successfully without custom instructions
|
||||||
expect(prompt).toContain("@claude please fix this");
|
expect(prompt).toContain("@claude please fix this");
|
||||||
@ -400,7 +318,7 @@ describe("generatePrompt", () => {
|
|||||||
envVars,
|
envVars,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockAgentMode,
|
"agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Agent mode: Prompt is passed through as-is
|
// Agent mode: Prompt is passed through as-is
|
||||||
@ -441,7 +359,7 @@ describe("generatePrompt", () => {
|
|||||||
envVars,
|
envVars,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockAgentMode,
|
"agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
|
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
|
||||||
@ -490,7 +408,7 @@ describe("generatePrompt", () => {
|
|||||||
envVars,
|
envVars,
|
||||||
issueGitHubData,
|
issueGitHubData,
|
||||||
false,
|
false,
|
||||||
mockAgentMode,
|
"agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Agent mode: Prompt is passed through as-is
|
// Agent mode: Prompt is passed through as-is
|
||||||
@ -515,7 +433,7 @@ describe("generatePrompt", () => {
|
|||||||
envVars,
|
envVars,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockAgentMode,
|
"agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Agent mode: No substitution - passed as-is
|
// Agent mode: No substitution - passed as-is
|
||||||
@ -539,12 +457,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||||
@ -567,12 +480,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||||
// With commit signing disabled, co-author info appears in git commit instructions
|
// With commit signing disabled, co-author info appears in git commit instructions
|
||||||
@ -594,12 +502,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain PR-specific instructions (git commands when not using signing)
|
// Should contain PR-specific instructions (git commands when not using signing)
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("git push");
|
||||||
@ -630,12 +533,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain Issue-specific instructions
|
// Should contain Issue-specific instructions
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -674,12 +572,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain the actual branch name with timestamp
|
// Should contain the actual branch name with timestamp
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -709,12 +602,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain branch-specific instructions like issues
|
// Should contain branch-specific instructions like issues
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -752,12 +640,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain open PR instructions (git commands when not using signing)
|
// Should contain open PR instructions (git commands when not using signing)
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("git push");
|
||||||
@ -788,12 +671,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain new branch instructions
|
// Should contain new branch instructions
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -821,12 +699,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain new branch instructions
|
// Should contain new branch instructions
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -854,12 +727,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain new branch instructions
|
// Should contain new branch instructions
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
@ -883,12 +751,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have git command instructions
|
// Should have git command instructions
|
||||||
expect(prompt).toContain("Use git commands via the Bash tool");
|
expect(prompt).toContain("Use git commands via the Bash tool");
|
||||||
@ -917,12 +780,7 @@ describe("generatePrompt", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = await generatePrompt(
|
const prompt = await generatePrompt(envVars, mockGitHubData, true, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
true,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have commit signing tool instructions
|
// Should have commit signing tool instructions
|
||||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||||
|
|||||||
@ -7,22 +7,17 @@ import {
|
|||||||
spyOn,
|
spyOn,
|
||||||
mock,
|
mock,
|
||||||
} from "bun:test";
|
} from "bun:test";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { prepareAgentMode } from "../../src/modes/agent";
|
||||||
import type { GitHubContext } from "../../src/github/context";
|
import { createMockAutomationContext } from "../mockContext";
|
||||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as gitConfig from "../../src/github/operations/git-config";
|
import * as gitConfig from "../../src/github/operations/git-config";
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: GitHubContext;
|
|
||||||
let exportVariableSpy: any;
|
let exportVariableSpy: any;
|
||||||
let setOutputSpy: any;
|
let setOutputSpy: any;
|
||||||
let configureGitAuthSpy: any;
|
let configureGitAuthSpy: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockAutomationContext({
|
|
||||||
eventName: "workflow_dispatch",
|
|
||||||
});
|
|
||||||
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
@ -45,84 +40,11 @@ describe("Agent Mode", () => {
|
|||||||
configureGitAuthSpy?.mockRestore();
|
configureGitAuthSpy?.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties", () => {
|
test("prepareAgentMode is exported as a function", () => {
|
||||||
expect(agentMode.name).toBe("agent");
|
expect(typeof prepareAgentMode).toBe("function");
|
||||||
expect(agentMode.description).toBe(
|
|
||||||
"Direct automation mode for explicit prompts",
|
|
||||||
);
|
|
||||||
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
|
||||||
expect(agentMode.getAllowedTools()).toEqual([]);
|
|
||||||
expect(agentMode.getDisallowedTools()).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prepareContext returns minimal data", () => {
|
test("prepare passes through claude_args", async () => {
|
||||||
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 () => {
|
|
||||||
// Clear any previous calls before this test
|
// Clear any previous calls before this test
|
||||||
exportVariableSpy.mockClear();
|
exportVariableSpy.mockClear();
|
||||||
setOutputSpy.mockClear();
|
setOutputSpy.mockClear();
|
||||||
@ -156,7 +78,7 @@ describe("Agent Mode", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
const result = await agentMode.prepare({
|
const result = await prepareAgentMode({
|
||||||
context: contextWithCustomArgs,
|
context: contextWithCustomArgs,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
githubToken: "test-token",
|
githubToken: "test-token",
|
||||||
@ -186,7 +108,7 @@ describe("Agent Mode", () => {
|
|||||||
process.env.GITHUB_REF_NAME = originalRefName;
|
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({
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
});
|
});
|
||||||
@ -206,7 +128,7 @@ describe("Agent Mode", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
agentMode.prepare({
|
prepareAgentMode({
|
||||||
context: contextWithPrompts,
|
context: contextWithPrompts,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
githubToken: "test-token",
|
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({
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
});
|
});
|
||||||
@ -237,7 +159,7 @@ describe("Agent Mode", () => {
|
|||||||
|
|
||||||
// Should not throw - bot is in allowed list
|
// Should not throw - bot is in allowed list
|
||||||
await expect(
|
await expect(
|
||||||
agentMode.prepare({
|
prepareAgentMode({
|
||||||
context: contextWithPrompts,
|
context: contextWithPrompts,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
githubToken: "test-token",
|
githubToken: "test-token",
|
||||||
@ -245,7 +167,7 @@ describe("Agent Mode", () => {
|
|||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prepare method creates prompt file with correct content", async () => {
|
test("prepare creates prompt file with correct content", async () => {
|
||||||
const contextWithPrompts = createMockAutomationContext({
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
});
|
});
|
||||||
@ -268,7 +190,7 @@ describe("Agent Mode", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
const result = await agentMode.prepare({
|
const result = await prepareAgentMode({
|
||||||
context: contextWithPrompts,
|
context: contextWithPrompts,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
githubToken: "test-token",
|
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 { describe, test, expect } from "bun:test";
|
||||||
import { tagMode } from "../../src/modes/tag";
|
import { prepareTagMode } from "../../src/modes/tag";
|
||||||
import type { ParsedGitHubContext } from "../../src/github/context";
|
|
||||||
import type { IssueCommentEvent } from "@octokit/webhooks-types";
|
|
||||||
import { createMockContext } from "../mockContext";
|
|
||||||
|
|
||||||
describe("Tag Mode", () => {
|
describe("Tag Mode", () => {
|
||||||
let mockContext: ParsedGitHubContext;
|
test("prepareTagMode is exported as a function", () => {
|
||||||
|
expect(typeof prepareTagMode).toBe("function");
|
||||||
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([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,38 +1,10 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import {
|
import { getEventTypeAndContext, generatePrompt } from "../src/create-prompt";
|
||||||
getEventTypeAndContext,
|
|
||||||
generatePrompt,
|
|
||||||
generateDefaultPrompt,
|
|
||||||
} from "../src/create-prompt";
|
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
import type { PreparedContext } from "../src/create-prompt";
|
||||||
import type { Mode } from "../src/modes/types";
|
|
||||||
|
|
||||||
describe("pull_request_target event support", () => {
|
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 = {
|
const mockGitHubData = {
|
||||||
contextData: {
|
contextData: {
|
||||||
title: "External PR via pull_request_target",
|
title: "External PR via pull_request_target",
|
||||||
@ -126,12 +98,7 @@ describe("pull_request_target event support", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain pull request event type and metadata
|
// Should contain pull request event type and metadata
|
||||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||||
@ -165,12 +132,7 @@ describe("pull_request_target event support", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should include git commands for non-commit-signing mode
|
// Should include git commands for non-commit-signing mode
|
||||||
expect(prompt).toContain("git push");
|
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
|
// Should include commit signing tools
|
||||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||||
@ -246,13 +208,13 @@ describe("pull_request_target event support", () => {
|
|||||||
pullRequestContext,
|
pullRequestContext,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockTagMode,
|
"tag",
|
||||||
);
|
);
|
||||||
const pullRequestTargetPrompt = generatePrompt(
|
const pullRequestTargetPrompt = generatePrompt(
|
||||||
pullRequestTargetContext,
|
pullRequestTargetContext,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockTagMode,
|
"tag",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Both should have the same event type and structure
|
// 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 prompt = generatePrompt(envVars, mockGitHubData, false, "agent");
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toBe(
|
expect(prompt).toBe(
|
||||||
"Review this pull_request_target PR for security issues",
|
"Review this pull_request_target PR for security issues",
|
||||||
@ -343,12 +275,7 @@ describe("pull_request_target event support", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should generate default prompt structure
|
// Should generate default prompt structure
|
||||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
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
|
// Should not throw when generating prompt
|
||||||
expect(() => {
|
expect(() => {
|
||||||
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
|
generatePrompt(minimalContext, mockGitHubData, false, "tag");
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -476,13 +403,13 @@ describe("pull_request_target event support", () => {
|
|||||||
internalPR,
|
internalPR,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockTagMode,
|
"tag",
|
||||||
);
|
);
|
||||||
const externalPrompt = generatePrompt(
|
const externalPrompt = generatePrompt(
|
||||||
externalPR,
|
externalPR,
|
||||||
mockGitHubData,
|
mockGitHubData,
|
||||||
false,
|
false,
|
||||||
mockTagMode,
|
"tag",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should have same tool access patterns
|
// Should have same tool access patterns
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user