From 96970dfa2de175bb2d679aeec2d73801a269f475 Mon Sep 17 00:00:00 2001 From: km-anthropic Date: Tue, 29 Jul 2025 08:56:26 -0700 Subject: [PATCH] refactor: simplify prepare logic with mode-specific implementations --- src/modes/agent/index.ts | 56 +++++++++++++++++++- src/modes/tag/index.ts | 88 ++++++++++++++++++++++++++++++- src/modes/types.ts | 24 +++++++++ src/prepare/automation-events.ts | 70 ------------------------- src/prepare/entity-events.ts | 89 -------------------------------- src/prepare/index.ts | 23 ++++----- src/tools/export.ts | 49 ++++++++++++++++++ 7 files changed, 226 insertions(+), 173 deletions(-) delete mode 100644 src/prepare/automation-events.ts delete mode 100644 src/prepare/entity-events.ts create mode 100644 src/tools/export.ts diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index fd78356..4d67c37 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -1,4 +1,8 @@ -import type { Mode } from "../types"; +import * as core from "@actions/core"; +import { mkdir, writeFile } from "fs/promises"; +import type { Mode, ModeOptions, ModeResult } from "../types"; +import { prepareMcpConfig } from "../../mcp/install-mcp-server"; +import { exportToolEnvironmentVariables } from "../../tools/export"; /** * Agent mode implementation. @@ -39,4 +43,54 @@ export const agentMode: Mode = { shouldCreateTrackingComment() { return false; }, + + async prepare({ context, githubToken }: ModeOptions): Promise { + // Agent mode is designed for automation events (workflow_dispatch, schedule) + // and potentially other events where we want full automation without tracking + + // Create prompt directory + await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, { + recursive: true, + }); + + // Write the prompt - either override_prompt, direct_prompt, or a minimal default + const promptContent = + context.inputs.overridePrompt || + context.inputs.directPrompt || + `Repository: ${context.repository.owner}/${context.repository.repo}`; + + await writeFile( + `${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`, + promptContent, + ); + + // Export tool environment variables + exportToolEnvironmentVariables(agentMode, context); + + // Get MCP configuration + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + const mcpConfig = await prepareMcpConfig({ + githubToken, + owner: context.repository.owner, + repo: context.repository.repo, + branch: "", // No specific branch for agent mode + baseBranch: "", // No base branch needed + additionalMcpConfig, + claudeCommentId: "", + allowedTools: context.inputs.allowedTools, + context, + }); + + core.setOutput("mcp_config", mcpConfig); + + return { + commentId: undefined, + branchInfo: { + baseBranch: "", + currentBranch: "", + claudeBranch: undefined, + }, + mcpConfig, + }; + }, }; diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index e2b14b3..5731c94 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -1,5 +1,13 @@ -import type { Mode } from "../types"; +import * as core from "@actions/core"; +import type { Mode, ModeOptions, ModeResult } from "../types"; import { checkContainsTrigger } from "../../github/validation/trigger"; +import { checkHumanActor } from "../../github/validation/actor"; +import { createInitialComment } from "../../github/operations/comments/create-initial"; +import { setupBranch } from "../../github/operations/branch"; +import { configureGitAuth } from "../../github/operations/git-config"; +import { prepareMcpConfig } from "../../mcp/install-mcp-server"; +import { fetchGitHubData } from "../../github/data/fetcher"; +import { createPrompt } from "../../create-prompt"; /** * Tag mode implementation. @@ -37,4 +45,82 @@ export const tagMode: Mode = { shouldCreateTrackingComment() { return true; }, + + async prepare({ + context, + octokit, + githubToken, + }: ModeOptions): Promise { + // Tag mode handles entity-based events (issues, PRs, comments) + + // Check if actor is human + await checkHumanActor(octokit.rest, context); + + // Create initial tracking comment + let commentId: number | undefined; + let commentData: + | Awaited> + | undefined; + if (this.shouldCreateTrackingComment()) { + commentData = await createInitialComment(octokit.rest, context); + commentId = commentData.id; + } + + // Fetch GitHub data - entity events always have entityNumber and isPR + if (!context.entityNumber || context.isPR === undefined) { + throw new Error("Entity events must have entityNumber and isPR defined"); + } + + const githubData = await fetchGitHubData({ + octokits: octokit, + repository: `${context.repository.owner}/${context.repository.repo}`, + prNumber: context.entityNumber.toString(), + isPR: context.isPR, + triggerUsername: context.actor, + }); + + // Setup branch + const branchInfo = await setupBranch(octokit, githubData, context); + + // Configure git authentication if not using commit signing + if (!context.inputs.useCommitSigning) { + try { + await configureGitAuth(githubToken, context, commentData?.user || null); + } catch (error) { + console.error("Failed to configure git authentication:", error); + throw error; + } + } + + // Create prompt file + const modeContext = this.prepareContext(context, { + commentId, + baseBranch: branchInfo.baseBranch, + claudeBranch: branchInfo.claudeBranch, + }); + + await createPrompt(tagMode, modeContext, githubData, context); + + // Get MCP configuration + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + const mcpConfig = await prepareMcpConfig({ + githubToken, + owner: context.repository.owner, + repo: context.repository.repo, + branch: branchInfo.claudeBranch || branchInfo.currentBranch, + baseBranch: branchInfo.baseBranch, + additionalMcpConfig, + claudeCommentId: commentId?.toString() || "", + allowedTools: context.inputs.allowedTools, + context, + }); + + core.setOutput("mcp_config", mcpConfig); + + return { + commentId, + branchInfo, + mcpConfig, + }; + }, }; diff --git a/src/modes/types.ts b/src/modes/types.ts index cd3d1b7..d8a0ae9 100644 --- a/src/modes/types.ts +++ b/src/modes/types.ts @@ -53,4 +53,28 @@ export type Mode = { * Determines if this mode should create a tracking comment */ shouldCreateTrackingComment(): boolean; + + /** + * Prepares the GitHub environment for this mode. + * Each mode decides how to handle different event types. + * @returns PrepareResult with commentId, branchInfo, and mcpConfig + */ + prepare(options: ModeOptions): Promise; +}; + +// Define types for mode prepare method to avoid circular dependencies +export type ModeOptions = { + context: ParsedGitHubContext; + octokit: any; // We'll use any to avoid circular dependency with Octokits + githubToken: string; +}; + +export type ModeResult = { + commentId?: number; + branchInfo: { + baseBranch: string; + claudeBranch?: string; + currentBranch: string; + }; + mcpConfig: string; }; diff --git a/src/prepare/automation-events.ts b/src/prepare/automation-events.ts deleted file mode 100644 index 08f6c51..0000000 --- a/src/prepare/automation-events.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Prepare logic for automation events (workflow_dispatch, schedule) - * These events don't have associated GitHub entities and require minimal setup - */ - -import * as core from "@actions/core"; -import { prepareMcpConfig } from "../mcp/install-mcp-server"; -import { createPrompt } from "../create-prompt"; -import { getDefaultBranch } from "../github/operations/default-branch"; -import type { PrepareOptions, PrepareResult } from "./types"; - -export async function prepareAutomationEvent({ - context, - octokit, - mode, - githubToken, -}: PrepareOptions): Promise { - // For automation events, we skip: - // - Human actor check (it's automation) - // - Tracking comment (no issue/PR to comment on) - // - GitHub data fetching (no entity to fetch) - // - Branch setup (use default branch or current branch) - - // Get the default branch or use the one specified in inputs - const baseBranch = - context.inputs.baseBranch || - (await getDefaultBranch( - octokit.rest, - context.repository.owner, - context.repository.repo, - )); - - // For automation events, we stay on the current branch (typically main/master) - const branchInfo = { - baseBranch, - currentBranch: baseBranch, - claudeBranch: undefined, - }; - - // Create prompt file with minimal context - const modeContext = mode.prepareContext(context, { - baseBranch: branchInfo.baseBranch, - claudeBranch: branchInfo.claudeBranch, - }); - - // Pass null for githubData since automation events don't have associated entities - await createPrompt(mode, modeContext, null, context); - - // Get MCP configuration - const additionalMcpConfig = process.env.MCP_CONFIG || ""; - const mcpConfig = await prepareMcpConfig({ - githubToken, - owner: context.repository.owner, - repo: context.repository.repo, - branch: branchInfo.currentBranch, - baseBranch: branchInfo.baseBranch, - additionalMcpConfig, - claudeCommentId: "", - allowedTools: context.inputs.allowedTools, - context, - }); - - core.setOutput("mcp_config", mcpConfig); - - return { - commentId: undefined, - branchInfo, - mcpConfig, - }; -} diff --git a/src/prepare/entity-events.ts b/src/prepare/entity-events.ts deleted file mode 100644 index 867ae0c..0000000 --- a/src/prepare/entity-events.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Prepare logic for entity-based events (issues, PRs, comments) - * These events have associated GitHub entities that need to be fetched and managed - */ - -import * as core from "@actions/core"; -import { checkHumanActor } from "../github/validation/actor"; -import { createInitialComment } from "../github/operations/comments/create-initial"; -import { setupBranch } from "../github/operations/branch"; -import { configureGitAuth } from "../github/operations/git-config"; -import { prepareMcpConfig } from "../mcp/install-mcp-server"; -import { fetchGitHubData } from "../github/data/fetcher"; -import { createPrompt } from "../create-prompt"; -import type { PrepareOptions, PrepareResult } from "./types"; - -export async function prepareEntityEvent({ - context, - octokit, - mode, - githubToken, -}: PrepareOptions): Promise { - // Check if actor is human - await checkHumanActor(octokit.rest, context); - - // Create initial tracking comment (mode-aware) - let commentId: number | undefined; - let commentData: Awaited> | undefined; - if (mode.shouldCreateTrackingComment()) { - commentData = await createInitialComment(octokit.rest, context); - commentId = commentData.id; - } - - // Fetch GitHub data - entity events always have entityNumber and isPR - if (!context.entityNumber || context.isPR === undefined) { - throw new Error("Entity events must have entityNumber and isPR defined"); - } - - const githubData = await fetchGitHubData({ - octokits: octokit, - repository: `${context.repository.owner}/${context.repository.repo}`, - prNumber: context.entityNumber.toString(), - isPR: context.isPR, - triggerUsername: context.actor, - }); - - // Setup branch - const branchInfo = await setupBranch(octokit, githubData, context); - - // Configure git authentication if not using commit signing - if (!context.inputs.useCommitSigning) { - try { - await configureGitAuth(githubToken, context, commentData?.user || null); - } catch (error) { - console.error("Failed to configure git authentication:", error); - throw error; - } - } - - // Create prompt file - const modeContext = mode.prepareContext(context, { - commentId, - baseBranch: branchInfo.baseBranch, - claudeBranch: branchInfo.claudeBranch, - }); - - await createPrompt(mode, modeContext, githubData, context); - - // Get MCP configuration - const additionalMcpConfig = process.env.MCP_CONFIG || ""; - const mcpConfig = await prepareMcpConfig({ - githubToken, - owner: context.repository.owner, - repo: context.repository.repo, - branch: branchInfo.claudeBranch || branchInfo.currentBranch, - baseBranch: branchInfo.baseBranch, - additionalMcpConfig, - claudeCommentId: commentId?.toString() || "", - allowedTools: context.inputs.allowedTools, - context, - }); - - core.setOutput("mcp_config", mcpConfig); - - return { - commentId, - branchInfo, - mcpConfig, - }; -} diff --git a/src/prepare/index.ts b/src/prepare/index.ts index 72071d0..6f42301 100644 --- a/src/prepare/index.ts +++ b/src/prepare/index.ts @@ -1,21 +1,20 @@ /** - * Main prepare module that routes to appropriate prepare logic based on event type + * Main prepare module that delegates to the mode's prepare method */ import type { PrepareOptions, PrepareResult } from "./types"; -import { prepareEntityEvent } from "./entity-events"; -import { prepareAutomationEvent } from "./automation-events"; - -const AUTOMATION_EVENTS = ["workflow_dispatch", "schedule"]; export async function prepare(options: PrepareOptions): Promise { - const { context } = options; + const { mode, context, octokit, githubToken } = options; - if (AUTOMATION_EVENTS.includes(context.eventName)) { - console.log(`Preparing automation event: ${context.eventName}`); - return prepareAutomationEvent(options); - } + console.log( + `Preparing with mode: ${mode.name} for event: ${context.eventName}`, + ); - console.log(`Preparing entity-based event: ${context.eventName}`); - return prepareEntityEvent(options); + // Delegate to the mode's prepare method + return mode.prepare({ + context, + octokit, + githubToken, + }); } diff --git a/src/tools/export.ts b/src/tools/export.ts new file mode 100644 index 0000000..5d37a2e --- /dev/null +++ b/src/tools/export.ts @@ -0,0 +1,49 @@ +/** + * Handles exporting tool-related environment variables + */ + +import * as core from "@actions/core"; +import type { Mode } from "../modes/types"; +import type { ParsedGitHubContext } from "../github/context"; +import { + buildAllowedToolsString, + buildDisallowedToolsString, +} from "../create-prompt/index"; + +export function exportToolEnvironmentVariables( + mode: Mode, + context: ParsedGitHubContext, +): void { + const hasActionsReadPermission = + context.inputs.additionalPermissions.get("actions") === "read" && + context.isPR; + + const modeAllowedTools = mode.getAllowedTools(); + const modeDisallowedTools = mode.getDisallowedTools(); + + // Combine with existing allowed tools + const combinedAllowedTools = [ + ...context.inputs.allowedTools, + ...modeAllowedTools, + ]; + const combinedDisallowedTools = [ + ...context.inputs.disallowedTools, + ...modeDisallowedTools, + ]; + + const allAllowedTools = buildAllowedToolsString( + combinedAllowedTools, + hasActionsReadPermission, + context.inputs.useCommitSigning, + ); + const allDisallowedTools = buildDisallowedToolsString( + combinedDisallowedTools, + combinedAllowedTools, + ); + + console.log(`Allowed tools: ${allAllowedTools}`); + console.log(`Disallowed tools: ${allDisallowedTools}`); + + core.exportVariable("ALLOWED_TOOLS", allAllowedTools); + core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools); +}