From ec0e9b4f87ab886a0d7241b8249caa42208066d1 Mon Sep 17 00:00:00 2001 From: km-anthropic Date: Tue, 29 Jul 2025 11:52:45 -0700 Subject: [PATCH] add schedule & workflow dispatch paths. Also make prepare logic conditional (#353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add agent mode for automation scenarios - Add agent mode that always triggers without checking for mentions - Implement Mode interface with support for mode-specific tool configuration - Add getAllowedTools() and getDisallowedTools() methods to Mode interface - Simplify tests by combining related test cases - Update documentation and examples to include agent mode - Fix TypeScript imports to prevent circular dependencies Agent mode is designed for automation and workflow_dispatch scenarios where Claude should always run without requiring trigger phrases. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Minor update to readme (from @main to @beta) * Since workflow_dispatch isn't in the base action, update the examples accordingly * minor formatting issue * Update to say beta instead of main * Fix missed tracking comment to be false * add schedule & workflow dispatch paths. Also make prepare logic conditional * tests * Add test workflow for workflow_dispatch functionality * Update workflow to use correct branch reference * remove test workflow dispatch file * minor lint update * update workflow dispatch agent example * minor lint update * refactor: simplify prepare logic with mode-specific implementations * ensure tag mode can't work with workflow dispatch and schedule tasks * simplify: remove workflow_dispatch/schedule from create-prompt - Remove workflow_dispatch and schedule event handling from create-prompt since agent mode doesn't use the standard prompt generation flow - Enforce mode compatibility at selection time in the registry instead of runtime validation in tag mode - Add explanatory comment in agent mode about why prompt file is needed - Update tests to reflect simplified event handling This reduces code duplication and makes the separation between tag mode (entity-based events) and agent mode (automation events) clearer. * simplify PR by making agent mode only work with workflow dispatch and schedule events * remove unnecessary changes * remove unnecessary changes from PR - Revert update-comment-link.ts changes (agent mode doesn't use this) - Revert create-initial.ts changes (agent mode doesn't create comments) - Remove unused default-branch.ts file - Revert install-mcp-server.ts changes (agent mode uses minimal MCP) These files are only used by tag mode for entity-based events, not needed for workflow_dispatch/schedule support via agent mode. * fix: handle optional entityNumber for TypeScript - Add runtime checks in files that require entityNumber - These files are only used by tag mode which always has entityNumber - Agent mode (workflow_dispatch/schedule) doesn't use these files * linting update --------- Co-authored-by: km-anthropic Co-authored-by: Claude --- examples/workflow-dispatch-agent.yml | 40 +++++++ src/create-prompt/index.ts | 19 ++-- src/entrypoints/prepare.ts | 75 ++----------- src/entrypoints/update-comment-link.ts | 9 +- src/github/context.ts | 49 +++++++- .../operations/comments/create-initial.ts | 16 ++- src/mcp/install-mcp-server.ts | 2 +- src/modes/agent/index.ts | 105 +++++++++++++++--- src/modes/registry.ts | 20 +++- src/modes/tag/index.ts | 82 +++++++++++++- src/modes/types.ts | 24 ++++ src/prepare/index.ts | 20 ++++ src/prepare/types.ts | 20 ++++ test/modes/agent.test.ts | 68 +++++------- test/modes/registry.test.ts | 45 +++++++- 15 files changed, 447 insertions(+), 147 deletions(-) create mode 100644 examples/workflow-dispatch-agent.yml create mode 100644 src/prepare/index.ts create mode 100644 src/prepare/types.ts diff --git a/examples/workflow-dispatch-agent.yml b/examples/workflow-dispatch-agent.yml new file mode 100644 index 0000000..1e72847 --- /dev/null +++ b/examples/workflow-dispatch-agent.yml @@ -0,0 +1,40 @@ +name: Claude Commit Analysis + +on: + workflow_dispatch: + inputs: + analysis_type: + description: "Type of analysis to perform" + required: true + type: choice + options: + - summarize-commit + - security-review + default: "summarize-commit" + +jobs: + analyze-commit: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 # Need at least 2 commits to analyze the latest + + - name: Run Claude Analysis + uses: anthropics/claude-code-action@beta + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Analyze the latest commit in this repository. + + ${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }} + + ${{ github.event.inputs.analysis_type == 'security-review' && 'Task: Review the latest commit for potential security vulnerabilities. Check for exposed secrets, insecure coding patterns, dependency vulnerabilities, or any other security concerns. Provide specific recommendations if issues are found.' || '' }} diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 27b3281..884e36b 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -125,8 +125,10 @@ export function prepareContext( const isPR = context.isPR; // Get PR/Issue number from entityNumber - const prNumber = isPR ? context.entityNumber.toString() : undefined; - const issueNumber = !isPR ? context.entityNumber.toString() : undefined; + const prNumber = + isPR && context.entityNumber ? context.entityNumber.toString() : undefined; + const issueNumber = + !isPR && context.entityNumber ? context.entityNumber.toString() : undefined; // Extract trigger username and comment data based on event type let triggerUsername: string | undefined; @@ -801,15 +803,18 @@ export async function createPrompt( context: ParsedGitHubContext, ) { try { - // Tag mode requires a comment ID - if (mode.name === "tag" && !modeContext.commentId) { - throw new Error("Tag mode requires a comment ID for prompt generation"); + // Prepare the context for prompt generation + let claudeCommentId: string = ""; + if (mode.name === "tag") { + if (!modeContext.commentId) { + throw new Error("Tag mode requires a comment ID for prompt generation"); + } + claudeCommentId = modeContext.commentId.toString(); } - // Prepare the context for prompt generation const preparedContext = prepareContext( context, - modeContext.commentId?.toString() || "", + claudeCommentId, modeContext.baseBranch, modeContext.claudeBranch, ); diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 6653c06..0523ff1 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -7,17 +7,11 @@ import * as core from "@actions/core"; import { setupGitHubToken } from "../github/token"; -import { checkHumanActor } from "../github/validation/actor"; import { checkWritePermissions } from "../github/validation/permissions"; -import { createInitialComment } from "../github/operations/comments/create-initial"; -import { setupBranch } from "../github/operations/branch"; -import { configureGitAuth } from "../github/operations/git-config"; -import { prepareMcpConfig } from "../mcp/install-mcp-server"; import { createOctokit } from "../github/api/client"; -import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; import { getMode } from "../modes/registry"; -import { createPrompt } from "../create-prompt"; +import { prepare } from "../prepare"; async function run() { try { @@ -40,7 +34,7 @@ async function run() { } // Step 4: Get mode and check trigger conditions - const mode = getMode(context.inputs.mode); + const mode = getMode(context.inputs.mode, context); const containsTrigger = mode.shouldTrigger(context); // Set output for action.yml to check @@ -51,65 +45,16 @@ async function run() { return; } - // Step 5: Check if actor is human - await checkHumanActor(octokit.rest, context); - - // Step 6: Create initial tracking comment (mode-aware) - // Some modes (e.g., agent mode) may not need tracking comments - let commentId: number | undefined; - let commentData: - | Awaited> - | undefined; - if (mode.shouldCreateTrackingComment()) { - commentData = await createInitialComment(octokit.rest, context); - commentId = commentData.id; - } - - // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) - const githubData = await fetchGitHubData({ - octokits: octokit, - repository: `${context.repository.owner}/${context.repository.repo}`, - prNumber: context.entityNumber.toString(), - isPR: context.isPR, - triggerUsername: context.actor, - }); - - // Step 8: Setup branch - const branchInfo = await setupBranch(octokit, githubData, context); - - // Step 9: Configure git authentication if not using commit signing - if (!context.inputs.useCommitSigning) { - try { - await configureGitAuth(githubToken, context, commentData?.user || null); - } catch (error) { - console.error("Failed to configure git authentication:", error); - throw error; - } - } - - // Step 10: Create prompt file - const modeContext = mode.prepareContext(context, { - commentId, - baseBranch: branchInfo.baseBranch, - claudeBranch: branchInfo.claudeBranch, - }); - - await createPrompt(mode, modeContext, githubData, context); - - // Step 11: Get MCP configuration - const additionalMcpConfig = process.env.MCP_CONFIG || ""; - const mcpConfig = await prepareMcpConfig({ - githubToken, - owner: context.repository.owner, - repo: context.repository.repo, - branch: branchInfo.claudeBranch || branchInfo.currentBranch, - baseBranch: branchInfo.baseBranch, - additionalMcpConfig, - claudeCommentId: commentId?.toString() || "", - allowedTools: context.inputs.allowedTools, + // Step 5: Use the new modular prepare function + const result = await prepare({ context, + octokit, + mode, + githubToken, }); - core.setOutput("mcp_config", mcpConfig); + + // Set the MCP config output + core.setOutput("mcp_config", result.mcpConfig); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(`Prepare step failed with error: ${errorMessage}`); diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 85b2455..6586931 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -24,6 +24,13 @@ async function run() { const context = parseGitHubContext(); const { owner, repo } = context.repository; + + // This script is only called for entity-based events + if (!context.entityNumber) { + throw new Error("update-comment-link requires an entity number"); + } + const entityNumber = context.entityNumber; + const octokit = createOctokit(githubToken); const serverUrl = GITHUB_SERVER_URL; @@ -73,7 +80,7 @@ async function run() { const { data: pr } = await octokit.rest.pulls.get({ owner, repo, - pull_number: context.entityNumber, + pull_number: entityNumber, }); console.log(`PR state: ${pr.state}`); console.log(`PR comments count: ${pr.comments}`); diff --git a/src/github/context.ts b/src/github/context.ts index 4e0d866..c1cf5ef 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -7,6 +7,33 @@ import type { PullRequestReviewEvent, PullRequestReviewCommentEvent, } from "@octokit/webhooks-types"; +// Custom types for GitHub Actions events that aren't webhooks +export type WorkflowDispatchEvent = { + action?: never; + inputs?: Record; + ref?: string; + repository: { + name: string; + owner: { + login: string; + }; + }; + sender: { + login: string; + }; + workflow: string; +}; + +export type ScheduleEvent = { + action?: never; + schedule?: string; + repository: { + name: string; + owner: { + login: string; + }; + }; +}; import type { ModeName } from "../modes/types"; import { DEFAULT_MODE, isValidMode } from "../modes/registry"; @@ -25,9 +52,11 @@ export type ParsedGitHubContext = { | IssueCommentEvent | PullRequestEvent | PullRequestReviewEvent - | PullRequestReviewCommentEvent; - entityNumber: number; - isPR: boolean; + | PullRequestReviewCommentEvent + | WorkflowDispatchEvent + | ScheduleEvent; + entityNumber?: number; + isPR?: boolean; inputs: { mode: ModeName; triggerPhrase: string; @@ -129,6 +158,20 @@ export function parseGitHubContext(): ParsedGitHubContext { isPR: true, }; } + case "workflow_dispatch": { + return { + ...commonFields, + payload: context.payload as unknown as WorkflowDispatchEvent, + // No entityNumber or isPR for workflow_dispatch + }; + } + case "schedule": { + return { + ...commonFields, + payload: context.payload as unknown as ScheduleEvent, + // No entityNumber or isPR for schedule + }; + } default: throw new Error(`Unsupported event type: ${context.eventName}`); } diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index 1243035..72fd378 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -22,6 +22,12 @@ export async function createInitialComment( ) { const { owner, repo } = context.repository; + // This function is only called for entity-based events + if (!context.entityNumber) { + throw new Error("createInitialComment requires an entity number"); + } + const entityNumber = context.entityNumber; + const jobRunLink = createJobRunLink(owner, repo, context.runId); const initialBody = createCommentBody(jobRunLink); @@ -36,7 +42,7 @@ export async function createInitialComment( const comments = await octokit.rest.issues.listComments({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, }); const existingComment = comments.data.find((comment) => { const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID; @@ -59,7 +65,7 @@ export async function createInitialComment( response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); } @@ -68,7 +74,7 @@ export async function createInitialComment( response = await octokit.rest.pulls.createReplyForReviewComment({ owner, repo, - pull_number: context.entityNumber, + pull_number: entityNumber, comment_id: context.payload.comment.id, body: initialBody, }); @@ -77,7 +83,7 @@ export async function createInitialComment( response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); } @@ -95,7 +101,7 @@ export async function createInitialComment( const response = await octokit.rest.issues.createComment({ owner, repo, - issue_number: context.entityNumber, + issue_number: entityNumber, body: initialBody, }); diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 35bb94c..eb261a4 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -141,7 +141,7 @@ export async function prepareMcpConfig( GITHUB_TOKEN: process.env.ACTIONS_TOKEN, REPO_OWNER: owner, REPO_NAME: repo, - PR_NUMBER: context.entityNumber.toString(), + PR_NUMBER: context.entityNumber?.toString() || "", RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp", }, }; diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index fd78356..a32260a 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -1,30 +1,31 @@ -import type { Mode } from "../types"; +import * as core from "@actions/core"; +import { mkdir, writeFile } from "fs/promises"; +import type { Mode, ModeOptions, ModeResult } from "../types"; /** * Agent mode implementation. * - * This mode is designed for automation and workflow_dispatch scenarios. - * It always triggers (no checking), allows highly flexible configurations, - * and works well with override_prompt for custom workflows. - * - * In the future, this mode could restrict certain tools for safety in automation contexts, - * e.g., disallowing WebSearch or limiting file system operations. + * This mode is specifically designed for automation events (workflow_dispatch and schedule). + * It bypasses the standard trigger checking and comment tracking used by tag mode, + * making it ideal for scheduled tasks and manual workflow runs. */ export const agentMode: Mode = { name: "agent", - description: "Automation mode that always runs without trigger checking", + description: "Automation mode for workflow_dispatch and schedule events", - shouldTrigger() { - return true; + shouldTrigger(context) { + // Only trigger for automation events + return ( + context.eventName === "workflow_dispatch" || + context.eventName === "schedule" + ); }, - prepareContext(context, data) { + prepareContext(context) { + // Agent mode doesn't use comment tracking or branch management return { mode: "agent", githubContext: context, - commentId: data?.commentId, - baseBranch: data?.baseBranch, - claudeBranch: data?.claudeBranch, }; }, @@ -39,4 +40,80 @@ export const agentMode: Mode = { shouldCreateTrackingComment() { return false; }, + + async prepare({ context }: ModeOptions): Promise { + // Agent mode handles automation events (workflow_dispatch, schedule) only + + // Create prompt directory + await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, { + recursive: true, + }); + + // Write the prompt file - the base action requires a prompt_file parameter, + // so we must create this file even though agent mode typically uses + // override_prompt or direct_prompt. If neither is provided, we write + // a minimal prompt with just the repository information. + const promptContent = + context.inputs.overridePrompt || + context.inputs.directPrompt || + `Repository: ${context.repository.owner}/${context.repository.repo}`; + + await writeFile( + `${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`, + promptContent, + ); + + // Export tool environment variables for agent mode + const baseTools = [ + "Edit", + "MultiEdit", + "Glob", + "Grep", + "LS", + "Read", + "Write", + ]; + + // Add user-specified tools + const allowedTools = [...baseTools, ...context.inputs.allowedTools]; + const disallowedTools = [ + "WebSearch", + "WebFetch", + ...context.inputs.disallowedTools, + ]; + + core.exportVariable("ALLOWED_TOOLS", allowedTools.join(",")); + core.exportVariable("DISALLOWED_TOOLS", disallowedTools.join(",")); + + // Agent mode uses a minimal MCP configuration + // We don't need comment servers or PR-specific tools for automation + const mcpConfig: any = { + mcpServers: {}, + }; + + // Add user-provided additional MCP config if any + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + if (additionalMcpConfig.trim()) { + try { + const additional = JSON.parse(additionalMcpConfig); + if (additional && typeof additional === "object") { + Object.assign(mcpConfig, additional); + } + } catch (error) { + core.warning(`Failed to parse additional MCP config: ${error}`); + } + } + + core.setOutput("mcp_config", JSON.stringify(mcpConfig)); + + return { + commentId: undefined, + branchInfo: { + baseBranch: "", + currentBranch: "", + claudeBranch: undefined, + }, + mcpConfig: JSON.stringify(mcpConfig), + }; + }, }; diff --git a/src/modes/registry.ts b/src/modes/registry.ts index 043137a..70d6c7e 100644 --- a/src/modes/registry.ts +++ b/src/modes/registry.ts @@ -13,6 +13,7 @@ import type { Mode, ModeName } from "./types"; import { tagMode } from "./tag"; import { agentMode } from "./agent"; +import type { ParsedGitHubContext } from "../github/context"; export const DEFAULT_MODE = "tag" as const; export const VALID_MODES = ["tag", "agent"] as const; @@ -27,12 +28,13 @@ const modes = { } as const satisfies Record; /** - * Retrieves a mode by name. + * Retrieves a mode by name and validates it can handle the event type. * @param name The mode name to retrieve + * @param context The GitHub context to validate against * @returns The requested mode - * @throws Error if the mode is not found + * @throws Error if the mode is not found or cannot handle the event */ -export function getMode(name: ModeName): Mode { +export function getMode(name: ModeName, context: ParsedGitHubContext): Mode { const mode = modes[name]; if (!mode) { const validModes = VALID_MODES.join("', '"); @@ -40,6 +42,18 @@ export function getMode(name: ModeName): Mode { `Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`, ); } + + // Validate mode can handle the event type + if ( + name === "tag" && + (context.eventName === "workflow_dispatch" || + context.eventName === "schedule") + ) { + throw new Error( + `Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`, + ); + } + return mode; } diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index e2b14b3..9f4ef45 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,76 @@ 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 + const commentData = await createInitialComment(octokit.rest, context); + const 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); + } 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/index.ts b/src/prepare/index.ts new file mode 100644 index 0000000..6f42301 --- /dev/null +++ b/src/prepare/index.ts @@ -0,0 +1,20 @@ +/** + * Main prepare module that delegates to the mode's prepare method + */ + +import type { PrepareOptions, PrepareResult } from "./types"; + +export async function prepare(options: PrepareOptions): Promise { + 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, + }); +} diff --git a/src/prepare/types.ts b/src/prepare/types.ts new file mode 100644 index 0000000..5fa8c19 --- /dev/null +++ b/src/prepare/types.ts @@ -0,0 +1,20 @@ +import type { ParsedGitHubContext } from "../github/context"; +import type { Octokits } from "../github/api/client"; +import type { Mode } from "../modes/types"; + +export type PrepareResult = { + commentId?: number; + branchInfo: { + baseBranch: string; + claudeBranch?: string; + currentBranch: string; + }; + mcpConfig: string; +}; + +export type PrepareOptions = { + context: ParsedGitHubContext; + octokit: Octokits; + mode: Mode; + githubToken: string; +}; diff --git a/test/modes/agent.test.ts b/test/modes/agent.test.ts index d6583c8..9302790 100644 --- a/test/modes/agent.test.ts +++ b/test/modes/agent.test.ts @@ -13,70 +13,52 @@ describe("Agent Mode", () => { }); }); - test("agent mode has correct properties and behavior", () => { - // Basic properties + test("agent mode has correct properties", () => { expect(agentMode.name).toBe("agent"); expect(agentMode.description).toBe( - "Automation mode that always runs without trigger checking", + "Automation mode for workflow_dispatch and schedule events", ); expect(agentMode.shouldCreateTrackingComment()).toBe(false); - - // Tool methods return empty arrays expect(agentMode.getAllowedTools()).toEqual([]); expect(agentMode.getDisallowedTools()).toEqual([]); - - // Always triggers regardless of context - const contextWithoutTrigger = createMockContext({ - eventName: "workflow_dispatch", - isPR: false, - inputs: { - ...createMockContext().inputs, - triggerPhrase: "@claude", - }, - payload: {} as any, - }); - expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true); }); - test("prepareContext includes all required data", () => { - const data = { - commentId: 789, - baseBranch: "develop", - claudeBranch: "claude/automated-task", - }; - - const context = agentMode.prepareContext(mockContext, data); - - expect(context.mode).toBe("agent"); - expect(context.githubContext).toBe(mockContext); - expect(context.commentId).toBe(789); - expect(context.baseBranch).toBe("develop"); - expect(context.claudeBranch).toBe("claude/automated-task"); - }); - - test("prepareContext works without data", () => { + test("prepareContext returns minimal data", () => { const context = agentMode.prepareContext(mockContext); expect(context.mode).toBe("agent"); expect(context.githubContext).toBe(mockContext); - expect(context.commentId).toBeUndefined(); - expect(context.baseBranch).toBeUndefined(); - expect(context.claudeBranch).toBeUndefined(); + // Agent mode doesn't use comment tracking or branch management + expect(Object.keys(context)).toEqual(["mode", "githubContext"]); }); - test("agent mode triggers for all event types", () => { - const events = [ + test("agent mode only triggers for workflow_dispatch and schedule events", () => { + // Should trigger for automation events + const workflowDispatchContext = createMockContext({ + eventName: "workflow_dispatch", + isPR: false, + }); + expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true); + + const scheduleContext = createMockContext({ + eventName: "schedule", + isPR: false, + }); + expect(agentMode.shouldTrigger(scheduleContext)).toBe(true); + + // Should NOT trigger for other events + const otherEvents = [ "push", - "schedule", - "workflow_dispatch", "repository_dispatch", "issue_comment", "pull_request", + "pull_request_review", + "issues", ]; - events.forEach((eventName) => { + otherEvents.forEach((eventName) => { const context = createMockContext({ eventName, isPR: false }); - expect(agentMode.shouldTrigger(context)).toBe(true); + expect(agentMode.shouldTrigger(context)).toBe(false); }); }); }); diff --git a/test/modes/registry.test.ts b/test/modes/registry.test.ts index 2e7b011..82e4915 100644 --- a/test/modes/registry.test.ts +++ b/test/modes/registry.test.ts @@ -3,23 +3,60 @@ import { getMode, isValidMode } from "../../src/modes/registry"; import type { ModeName } from "../../src/modes/types"; import { tagMode } from "../../src/modes/tag"; import { agentMode } from "../../src/modes/agent"; +import { createMockContext } from "../mockContext"; describe("Mode Registry", () => { - test("getMode returns tag mode by default", () => { - const mode = getMode("tag"); + const mockContext = createMockContext({ + eventName: "issue_comment", + }); + + const mockWorkflowDispatchContext = createMockContext({ + eventName: "workflow_dispatch", + }); + + const mockScheduleContext = createMockContext({ + eventName: "schedule", + }); + + test("getMode returns tag mode for standard events", () => { + const mode = getMode("tag", mockContext); expect(mode).toBe(tagMode); expect(mode.name).toBe("tag"); }); test("getMode returns agent mode", () => { - const mode = getMode("agent"); + const mode = getMode("agent", mockContext); + expect(mode).toBe(agentMode); + expect(mode.name).toBe("agent"); + }); + + test("getMode throws error for tag mode with workflow_dispatch event", () => { + expect(() => getMode("tag", mockWorkflowDispatchContext)).toThrow( + "Tag mode cannot handle workflow_dispatch events. Use 'agent' mode for automation events.", + ); + }); + + test("getMode throws error for tag mode with schedule event", () => { + expect(() => getMode("tag", mockScheduleContext)).toThrow( + "Tag mode cannot handle schedule events. Use 'agent' mode for automation events.", + ); + }); + + test("getMode allows agent mode for workflow_dispatch event", () => { + const mode = getMode("agent", mockWorkflowDispatchContext); + expect(mode).toBe(agentMode); + expect(mode.name).toBe("agent"); + }); + + test("getMode allows agent mode for schedule event", () => { + const mode = getMode("agent", mockScheduleContext); expect(mode).toBe(agentMode); expect(mode.name).toBe("agent"); }); test("getMode throws error for invalid mode", () => { const invalidMode = "invalid" as unknown as ModeName; - expect(() => getMode(invalidMode)).toThrow( + expect(() => getMode(invalidMode, mockContext)).toThrow( "Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.", ); });