refactor: simplify prepare logic with mode-specific implementations

This commit is contained in:
km-anthropic
2025-07-29 08:56:26 -07:00
parent 11d4e4c175
commit 96970dfa2d
7 changed files with 226 additions and 173 deletions

View File

@@ -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<ModeResult> {
// 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,
};
},
};

View File

@@ -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<ModeResult> {
// 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<ReturnType<typeof createInitialComment>>
| 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,
};
},
};

View File

@@ -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<ModeResult>;
};
// 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;
};

View File

@@ -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<PrepareResult> {
// 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,
};
}

View File

@@ -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<PrepareResult> {
// Check if actor is human
await checkHumanActor(octokit.rest, context);
// Create initial tracking comment (mode-aware)
let commentId: number | undefined;
let commentData: Awaited<ReturnType<typeof createInitialComment>> | 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,
};
}

View File

@@ -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<PrepareResult> {
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,
});
}

49
src/tools/export.ts Normal file
View File

@@ -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);
}