mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
add schedule & workflow dispatch paths. Also make prepare logic conditional (#353)
* 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 <noreply@anthropic.com> * 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 <km-anthropic@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
40
examples/workflow-dispatch-agent.yml
Normal file
40
examples/workflow-dispatch-agent.yml
Normal file
@@ -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.' || '' }}
|
||||||
@@ -125,8 +125,10 @@ export function prepareContext(
|
|||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
// Get PR/Issue number from entityNumber
|
// Get PR/Issue number from entityNumber
|
||||||
const prNumber = isPR ? context.entityNumber.toString() : undefined;
|
const prNumber =
|
||||||
const issueNumber = !isPR ? context.entityNumber.toString() : undefined;
|
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
|
// Extract trigger username and comment data based on event type
|
||||||
let triggerUsername: string | undefined;
|
let triggerUsername: string | undefined;
|
||||||
@@ -801,15 +803,18 @@ export async function createPrompt(
|
|||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Tag mode requires a comment ID
|
// Prepare the context for prompt generation
|
||||||
if (mode.name === "tag" && !modeContext.commentId) {
|
let claudeCommentId: string = "";
|
||||||
|
if (mode.name === "tag") {
|
||||||
|
if (!modeContext.commentId) {
|
||||||
throw new Error("Tag mode requires a comment ID for prompt generation");
|
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(
|
const preparedContext = prepareContext(
|
||||||
context,
|
context,
|
||||||
modeContext.commentId?.toString() || "",
|
claudeCommentId,
|
||||||
modeContext.baseBranch,
|
modeContext.baseBranch,
|
||||||
modeContext.claudeBranch,
|
modeContext.claudeBranch,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,17 +7,11 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { setupGitHubToken } from "../github/token";
|
import { setupGitHubToken } from "../github/token";
|
||||||
import { checkHumanActor } from "../github/validation/actor";
|
|
||||||
import { checkWritePermissions } from "../github/validation/permissions";
|
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 { createOctokit } from "../github/api/client";
|
||||||
import { fetchGitHubData } from "../github/data/fetcher";
|
|
||||||
import { parseGitHubContext } from "../github/context";
|
import { parseGitHubContext } from "../github/context";
|
||||||
import { getMode } from "../modes/registry";
|
import { getMode } from "../modes/registry";
|
||||||
import { createPrompt } from "../create-prompt";
|
import { prepare } from "../prepare";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +34,7 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Get mode and check trigger conditions
|
// 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);
|
const containsTrigger = mode.shouldTrigger(context);
|
||||||
|
|
||||||
// Set output for action.yml to check
|
// Set output for action.yml to check
|
||||||
@@ -51,65 +45,16 @@ async function run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Check if actor is human
|
// Step 5: Use the new modular prepare function
|
||||||
await checkHumanActor(octokit.rest, context);
|
const result = await prepare({
|
||||||
|
|
||||||
// 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<ReturnType<typeof createInitialComment>>
|
|
||||||
| 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,
|
|
||||||
context,
|
context,
|
||||||
|
octokit,
|
||||||
|
mode,
|
||||||
|
githubToken,
|
||||||
});
|
});
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
|
||||||
|
// Set the MCP config output
|
||||||
|
core.setOutput("mcp_config", result.mcpConfig);
|
||||||
} 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}`);
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ async function run() {
|
|||||||
|
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
const { owner, repo } = context.repository;
|
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 octokit = createOctokit(githubToken);
|
||||||
|
|
||||||
const serverUrl = GITHUB_SERVER_URL;
|
const serverUrl = GITHUB_SERVER_URL;
|
||||||
@@ -73,7 +80,7 @@ async function run() {
|
|||||||
const { data: pr } = await octokit.rest.pulls.get({
|
const { data: pr } = await octokit.rest.pulls.get({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number: context.entityNumber,
|
pull_number: entityNumber,
|
||||||
});
|
});
|
||||||
console.log(`PR state: ${pr.state}`);
|
console.log(`PR state: ${pr.state}`);
|
||||||
console.log(`PR comments count: ${pr.comments}`);
|
console.log(`PR comments count: ${pr.comments}`);
|
||||||
|
|||||||
@@ -7,6 +7,33 @@ import type {
|
|||||||
PullRequestReviewEvent,
|
PullRequestReviewEvent,
|
||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
|
// Custom types for GitHub Actions events that aren't webhooks
|
||||||
|
export type WorkflowDispatchEvent = {
|
||||||
|
action?: never;
|
||||||
|
inputs?: Record<string, any>;
|
||||||
|
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 type { ModeName } from "../modes/types";
|
||||||
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
||||||
|
|
||||||
@@ -25,9 +52,11 @@ export type ParsedGitHubContext = {
|
|||||||
| IssueCommentEvent
|
| IssueCommentEvent
|
||||||
| PullRequestEvent
|
| PullRequestEvent
|
||||||
| PullRequestReviewEvent
|
| PullRequestReviewEvent
|
||||||
| PullRequestReviewCommentEvent;
|
| PullRequestReviewCommentEvent
|
||||||
entityNumber: number;
|
| WorkflowDispatchEvent
|
||||||
isPR: boolean;
|
| ScheduleEvent;
|
||||||
|
entityNumber?: number;
|
||||||
|
isPR?: boolean;
|
||||||
inputs: {
|
inputs: {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
triggerPhrase: string;
|
triggerPhrase: string;
|
||||||
@@ -129,6 +158,20 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
isPR: true,
|
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:
|
default:
|
||||||
throw new Error(`Unsupported event type: ${context.eventName}`);
|
throw new Error(`Unsupported event type: ${context.eventName}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export async function createInitialComment(
|
|||||||
) {
|
) {
|
||||||
const { owner, repo } = context.repository;
|
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 jobRunLink = createJobRunLink(owner, repo, context.runId);
|
||||||
const initialBody = createCommentBody(jobRunLink);
|
const initialBody = createCommentBody(jobRunLink);
|
||||||
|
|
||||||
@@ -36,7 +42,7 @@ export async function createInitialComment(
|
|||||||
const comments = await octokit.rest.issues.listComments({
|
const comments = await octokit.rest.issues.listComments({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: context.entityNumber,
|
issue_number: entityNumber,
|
||||||
});
|
});
|
||||||
const existingComment = comments.data.find((comment) => {
|
const existingComment = comments.data.find((comment) => {
|
||||||
const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID;
|
const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID;
|
||||||
@@ -59,7 +65,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.issues.createComment({
|
response = await octokit.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: context.entityNumber,
|
issue_number: entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,7 +74,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.pulls.createReplyForReviewComment({
|
response = await octokit.rest.pulls.createReplyForReviewComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number: context.entityNumber,
|
pull_number: entityNumber,
|
||||||
comment_id: context.payload.comment.id,
|
comment_id: context.payload.comment.id,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
@@ -77,7 +83,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.issues.createComment({
|
response = await octokit.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: context.entityNumber,
|
issue_number: entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,7 +101,7 @@ export async function createInitialComment(
|
|||||||
const response = await octokit.rest.issues.createComment({
|
const response = await octokit.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: context.entityNumber,
|
issue_number: entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export async function prepareMcpConfig(
|
|||||||
GITHUB_TOKEN: process.env.ACTIONS_TOKEN,
|
GITHUB_TOKEN: process.env.ACTIONS_TOKEN,
|
||||||
REPO_OWNER: owner,
|
REPO_OWNER: owner,
|
||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
PR_NUMBER: context.entityNumber.toString(),
|
PR_NUMBER: context.entityNumber?.toString() || "",
|
||||||
RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp",
|
RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
* Agent mode implementation.
|
||||||
*
|
*
|
||||||
* This mode is designed for automation and workflow_dispatch scenarios.
|
* This mode is specifically designed for automation events (workflow_dispatch and schedule).
|
||||||
* It always triggers (no checking), allows highly flexible configurations,
|
* It bypasses the standard trigger checking and comment tracking used by tag mode,
|
||||||
* and works well with override_prompt for custom workflows.
|
* making it ideal for scheduled tasks and manual workflow runs.
|
||||||
*
|
|
||||||
* In the future, this mode could restrict certain tools for safety in automation contexts,
|
|
||||||
* e.g., disallowing WebSearch or limiting file system operations.
|
|
||||||
*/
|
*/
|
||||||
export const agentMode: Mode = {
|
export const agentMode: Mode = {
|
||||||
name: "agent",
|
name: "agent",
|
||||||
description: "Automation mode that always runs without trigger checking",
|
description: "Automation mode for workflow_dispatch and schedule events",
|
||||||
|
|
||||||
shouldTrigger() {
|
shouldTrigger(context) {
|
||||||
return true;
|
// 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 {
|
return {
|
||||||
mode: "agent",
|
mode: "agent",
|
||||||
githubContext: context,
|
githubContext: context,
|
||||||
commentId: data?.commentId,
|
|
||||||
baseBranch: data?.baseBranch,
|
|
||||||
claudeBranch: data?.claudeBranch,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,4 +40,80 @@ export const agentMode: Mode = {
|
|||||||
shouldCreateTrackingComment() {
|
shouldCreateTrackingComment() {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async prepare({ context }: ModeOptions): Promise<ModeResult> {
|
||||||
|
// 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),
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import type { Mode, ModeName } from "./types";
|
import type { Mode, ModeName } from "./types";
|
||||||
import { tagMode } from "./tag";
|
import { tagMode } from "./tag";
|
||||||
import { agentMode } from "./agent";
|
import { agentMode } from "./agent";
|
||||||
|
import type { ParsedGitHubContext } from "../github/context";
|
||||||
|
|
||||||
export const DEFAULT_MODE = "tag" as const;
|
export const DEFAULT_MODE = "tag" as const;
|
||||||
export const VALID_MODES = ["tag", "agent"] as const;
|
export const VALID_MODES = ["tag", "agent"] as const;
|
||||||
@@ -27,12 +28,13 @@ const modes = {
|
|||||||
} as const satisfies Record<ModeName, Mode>;
|
} as const satisfies Record<ModeName, Mode>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 name The mode name to retrieve
|
||||||
|
* @param context The GitHub context to validate against
|
||||||
* @returns The requested mode
|
* @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];
|
const mode = modes[name];
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
const validModes = VALID_MODES.join("', '");
|
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.`,
|
`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;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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.
|
* Tag mode implementation.
|
||||||
@@ -37,4 +45,76 @@ export const tagMode: Mode = {
|
|||||||
shouldCreateTrackingComment() {
|
shouldCreateTrackingComment() {
|
||||||
return true;
|
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
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,4 +53,28 @@ export type Mode = {
|
|||||||
* Determines if this mode should create a tracking comment
|
* Determines if this mode should create a tracking comment
|
||||||
*/
|
*/
|
||||||
shouldCreateTrackingComment(): boolean;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/prepare/index.ts
Normal file
20
src/prepare/index.ts
Normal file
@@ -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<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,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
src/prepare/types.ts
Normal file
20
src/prepare/types.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -13,70 +13,52 @@ describe("Agent Mode", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties and behavior", () => {
|
test("agent mode has correct properties", () => {
|
||||||
// Basic properties
|
|
||||||
expect(agentMode.name).toBe("agent");
|
expect(agentMode.name).toBe("agent");
|
||||||
expect(agentMode.description).toBe(
|
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);
|
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
||||||
|
|
||||||
// Tool methods return empty arrays
|
|
||||||
expect(agentMode.getAllowedTools()).toEqual([]);
|
expect(agentMode.getAllowedTools()).toEqual([]);
|
||||||
expect(agentMode.getDisallowedTools()).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", () => {
|
test("prepareContext returns minimal 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", () => {
|
|
||||||
const context = agentMode.prepareContext(mockContext);
|
const context = agentMode.prepareContext(mockContext);
|
||||||
|
|
||||||
expect(context.mode).toBe("agent");
|
expect(context.mode).toBe("agent");
|
||||||
expect(context.githubContext).toBe(mockContext);
|
expect(context.githubContext).toBe(mockContext);
|
||||||
expect(context.commentId).toBeUndefined();
|
// Agent mode doesn't use comment tracking or branch management
|
||||||
expect(context.baseBranch).toBeUndefined();
|
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
|
||||||
expect(context.claudeBranch).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode triggers for all event types", () => {
|
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
|
||||||
const 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",
|
"push",
|
||||||
"schedule",
|
|
||||||
"workflow_dispatch",
|
|
||||||
"repository_dispatch",
|
"repository_dispatch",
|
||||||
"issue_comment",
|
"issue_comment",
|
||||||
"pull_request",
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"issues",
|
||||||
];
|
];
|
||||||
|
|
||||||
events.forEach((eventName) => {
|
otherEvents.forEach((eventName) => {
|
||||||
const context = createMockContext({ eventName, isPR: false });
|
const context = createMockContext({ eventName, isPR: false });
|
||||||
expect(agentMode.shouldTrigger(context)).toBe(true);
|
expect(agentMode.shouldTrigger(context)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,23 +3,60 @@ import { getMode, isValidMode } from "../../src/modes/registry";
|
|||||||
import type { ModeName } from "../../src/modes/types";
|
import type { ModeName } from "../../src/modes/types";
|
||||||
import { tagMode } from "../../src/modes/tag";
|
import { tagMode } from "../../src/modes/tag";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
|
import { createMockContext } from "../mockContext";
|
||||||
|
|
||||||
describe("Mode Registry", () => {
|
describe("Mode Registry", () => {
|
||||||
test("getMode returns tag mode by default", () => {
|
const mockContext = createMockContext({
|
||||||
const mode = getMode("tag");
|
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).toBe(tagMode);
|
||||||
expect(mode.name).toBe("tag");
|
expect(mode.name).toBe("tag");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMode returns agent mode", () => {
|
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).toBe(agentMode);
|
||||||
expect(mode.name).toBe("agent");
|
expect(mode.name).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMode throws error for invalid mode", () => {
|
test("getMode throws error for invalid mode", () => {
|
||||||
const invalidMode = "invalid" as unknown as ModeName;
|
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.",
|
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user