* 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>
242 lines
6.7 KiB
TypeScript
242 lines
6.7 KiB
TypeScript
import * as github from "@actions/github";
|
|
import type {
|
|
IssuesEvent,
|
|
IssuesAssignedEvent,
|
|
IssueCommentEvent,
|
|
PullRequestEvent,
|
|
PullRequestReviewEvent,
|
|
PullRequestReviewCommentEvent,
|
|
} 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 { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
|
|
|
export type ParsedGitHubContext = {
|
|
runId: string;
|
|
eventName: string;
|
|
eventAction?: string;
|
|
repository: {
|
|
owner: string;
|
|
repo: string;
|
|
full_name: string;
|
|
};
|
|
actor: string;
|
|
payload:
|
|
| IssuesEvent
|
|
| IssueCommentEvent
|
|
| PullRequestEvent
|
|
| PullRequestReviewEvent
|
|
| PullRequestReviewCommentEvent
|
|
| WorkflowDispatchEvent
|
|
| ScheduleEvent;
|
|
entityNumber?: number;
|
|
isPR?: boolean;
|
|
inputs: {
|
|
mode: ModeName;
|
|
triggerPhrase: string;
|
|
assigneeTrigger: string;
|
|
labelTrigger: string;
|
|
allowedTools: string[];
|
|
disallowedTools: string[];
|
|
customInstructions: string;
|
|
directPrompt: string;
|
|
overridePrompt: string;
|
|
baseBranch?: string;
|
|
branchPrefix: string;
|
|
useStickyComment: boolean;
|
|
additionalPermissions: Map<string, string>;
|
|
useCommitSigning: boolean;
|
|
};
|
|
};
|
|
|
|
export function parseGitHubContext(): ParsedGitHubContext {
|
|
const context = github.context;
|
|
|
|
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
|
if (!isValidMode(modeInput)) {
|
|
throw new Error(`Invalid mode: ${modeInput}.`);
|
|
}
|
|
|
|
const commonFields = {
|
|
runId: process.env.GITHUB_RUN_ID!,
|
|
eventName: context.eventName,
|
|
eventAction: context.payload.action,
|
|
repository: {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
full_name: `${context.repo.owner}/${context.repo.repo}`,
|
|
},
|
|
actor: context.actor,
|
|
inputs: {
|
|
mode: modeInput as ModeName,
|
|
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
|
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
|
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
|
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
|
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
|
|
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
|
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
|
overridePrompt: process.env.OVERRIDE_PROMPT ?? "",
|
|
baseBranch: process.env.BASE_BRANCH,
|
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
|
additionalPermissions: parseAdditionalPermissions(
|
|
process.env.ADDITIONAL_PERMISSIONS ?? "",
|
|
),
|
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
|
},
|
|
};
|
|
|
|
switch (context.eventName) {
|
|
case "issues": {
|
|
return {
|
|
...commonFields,
|
|
payload: context.payload as IssuesEvent,
|
|
entityNumber: (context.payload as IssuesEvent).issue.number,
|
|
isPR: false,
|
|
};
|
|
}
|
|
case "issue_comment": {
|
|
return {
|
|
...commonFields,
|
|
payload: context.payload as IssueCommentEvent,
|
|
entityNumber: (context.payload as IssueCommentEvent).issue.number,
|
|
isPR: Boolean(
|
|
(context.payload as IssueCommentEvent).issue.pull_request,
|
|
),
|
|
};
|
|
}
|
|
case "pull_request": {
|
|
return {
|
|
...commonFields,
|
|
payload: context.payload as PullRequestEvent,
|
|
entityNumber: (context.payload as PullRequestEvent).pull_request.number,
|
|
isPR: true,
|
|
};
|
|
}
|
|
case "pull_request_review": {
|
|
return {
|
|
...commonFields,
|
|
payload: context.payload as PullRequestReviewEvent,
|
|
entityNumber: (context.payload as PullRequestReviewEvent).pull_request
|
|
.number,
|
|
isPR: true,
|
|
};
|
|
}
|
|
case "pull_request_review_comment": {
|
|
return {
|
|
...commonFields,
|
|
payload: context.payload as PullRequestReviewCommentEvent,
|
|
entityNumber: (context.payload as PullRequestReviewCommentEvent)
|
|
.pull_request.number,
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
export function parseMultilineInput(s: string): string[] {
|
|
return s
|
|
.split(/,|[\n\r]+/)
|
|
.map((tool) => tool.replace(/#.+$/, ""))
|
|
.map((tool) => tool.trim())
|
|
.filter((tool) => tool.length > 0);
|
|
}
|
|
|
|
export function parseAdditionalPermissions(s: string): Map<string, string> {
|
|
const permissions = new Map<string, string>();
|
|
if (!s || !s.trim()) {
|
|
return permissions;
|
|
}
|
|
|
|
const lines = s.trim().split("\n");
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine) {
|
|
const [key, value] = trimmedLine.split(":").map((part) => part.trim());
|
|
if (key && value) {
|
|
permissions.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
return permissions;
|
|
}
|
|
|
|
export function isIssuesEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
|
return context.eventName === "issues";
|
|
}
|
|
|
|
export function isIssueCommentEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
|
return context.eventName === "issue_comment";
|
|
}
|
|
|
|
export function isPullRequestEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
|
return context.eventName === "pull_request";
|
|
}
|
|
|
|
export function isPullRequestReviewEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
|
return context.eventName === "pull_request_review";
|
|
}
|
|
|
|
export function isPullRequestReviewCommentEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
|
return context.eventName === "pull_request_review_comment";
|
|
}
|
|
|
|
export function isIssuesAssignedEvent(
|
|
context: ParsedGitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
|
return isIssuesEvent(context) && context.eventAction === "assigned";
|
|
}
|