mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
refactor: implement discriminated unions for GitHub contexts (#360)
* 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 * refactor: implement discriminated unions for GitHub contexts Split ParsedGitHubContext into entity-specific and automation contexts: - ParsedGitHubContext: For entity events (issues/PRs) with required entityNumber and isPR - AutomationContext: For workflow_dispatch/schedule events without entity fields - GitHubContext: Union type for all contexts This eliminates ~20 null checks throughout the codebase and provides better type safety. Entity-specific code paths are now guaranteed to have the required fields. Co-Authored-By: Claude <noreply@anthropic.com> * update comment * More robust type checking * refactor: improve discriminated union implementation based on review feedback - Use eventName checks instead of 'in' operator for more robust type guards - Remove unnecessary type assertions - TypeScript's control flow analysis works correctly - Remove redundant runtime checks for entityNumber and isPR - Simplify code by using context directly after type guard Co-Authored-By: Claude <noreply@anthropic.com> * some structural simplification * refactor: further simplify discriminated union implementation - Add event name constants to reduce duplication - Derive EntityEventName and AutomationEventName types from constants - Use isAutomationContext consistently in agent mode and registry - Simplify parseGitHubContext by removing redundant type assertions - Extract payload casts to variables for cleaner code Co-Authored-By: Claude <noreply@anthropic.com> * bun format * specify the type * minor linting update again --------- Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -125,10 +125,8 @@ 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 =
|
const prNumber = isPR ? context.entityNumber.toString() : undefined;
|
||||||
isPR && context.entityNumber ? context.entityNumber.toString() : undefined;
|
const issueNumber = !isPR ? 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;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as core from "@actions/core";
|
|||||||
import { setupGitHubToken } from "../github/token";
|
import { setupGitHubToken } from "../github/token";
|
||||||
import { checkWritePermissions } from "../github/validation/permissions";
|
import { checkWritePermissions } from "../github/validation/permissions";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
import { parseGitHubContext } from "../github/context";
|
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||||
import { getMode } from "../modes/registry";
|
import { getMode } from "../modes/registry";
|
||||||
import { prepare } from "../prepare";
|
import { prepare } from "../prepare";
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ async function run() {
|
|||||||
// Step 2: Parse GitHub context (once for all operations)
|
// Step 2: Parse GitHub context (once for all operations)
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
|
|
||||||
// Step 3: Check write permissions
|
// Step 3: Check write permissions (only for entity contexts)
|
||||||
|
if (isEntityContext(context)) {
|
||||||
const hasWritePermissions = await checkWritePermissions(
|
const hasWritePermissions = await checkWritePermissions(
|
||||||
octokit.rest,
|
octokit.rest,
|
||||||
context,
|
context,
|
||||||
@@ -32,6 +33,7 @@ async function run() {
|
|||||||
"Actor does not have write permissions to the repository",
|
"Actor does not have write permissions to the repository",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 4: Get mode and check trigger conditions
|
// Step 4: Get mode and check trigger conditions
|
||||||
const mode = getMode(context.inputs.mode, context);
|
const mode = getMode(context.inputs.mode, context);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
parseGitHubContext,
|
parseGitHubContext,
|
||||||
isPullRequestReviewCommentEvent,
|
isPullRequestReviewCommentEvent,
|
||||||
|
isEntityContext,
|
||||||
} from "../github/context";
|
} from "../github/context";
|
||||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||||
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
||||||
@@ -23,13 +24,13 @@ async function run() {
|
|||||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||||
|
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
const { owner, repo } = context.repository;
|
|
||||||
|
|
||||||
// This script is only called for entity-based events
|
// This script is only called for entity-based events
|
||||||
if (!context.entityNumber) {
|
if (!isEntityContext(context)) {
|
||||||
throw new Error("update-comment-link requires an entity number");
|
throw new Error("update-comment-link requires an entity context");
|
||||||
}
|
}
|
||||||
const entityNumber = context.entityNumber;
|
|
||||||
|
const { owner, repo } = context.repository;
|
||||||
|
|
||||||
const octokit = createOctokit(githubToken);
|
const octokit = createOctokit(githubToken);
|
||||||
|
|
||||||
@@ -80,7 +81,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: entityNumber,
|
pull_number: context.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}`);
|
||||||
|
|||||||
@@ -37,9 +37,24 @@ export type ScheduleEvent = {
|
|||||||
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";
|
||||||
|
|
||||||
export type ParsedGitHubContext = {
|
// Event name constants for better maintainability
|
||||||
|
const ENTITY_EVENT_NAMES = [
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const AUTOMATION_EVENT_NAMES = ["workflow_dispatch", "schedule"] as const;
|
||||||
|
|
||||||
|
// Derive types from constants for better maintainability
|
||||||
|
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
|
||||||
|
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
|
||||||
|
|
||||||
|
// Common fields shared by all context types
|
||||||
|
type BaseContext = {
|
||||||
runId: string;
|
runId: string;
|
||||||
eventName: string;
|
|
||||||
eventAction?: string;
|
eventAction?: string;
|
||||||
repository: {
|
repository: {
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -47,16 +62,6 @@ export type ParsedGitHubContext = {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
};
|
};
|
||||||
actor: string;
|
actor: string;
|
||||||
payload:
|
|
||||||
| IssuesEvent
|
|
||||||
| IssueCommentEvent
|
|
||||||
| PullRequestEvent
|
|
||||||
| PullRequestReviewEvent
|
|
||||||
| PullRequestReviewCommentEvent
|
|
||||||
| WorkflowDispatchEvent
|
|
||||||
| ScheduleEvent;
|
|
||||||
entityNumber?: number;
|
|
||||||
isPR?: boolean;
|
|
||||||
inputs: {
|
inputs: {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
triggerPhrase: string;
|
triggerPhrase: string;
|
||||||
@@ -75,7 +80,29 @@ export type ParsedGitHubContext = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseGitHubContext(): ParsedGitHubContext {
|
// Context for entity-based events (issues, PRs, comments)
|
||||||
|
export type ParsedGitHubContext = BaseContext & {
|
||||||
|
eventName: EntityEventName;
|
||||||
|
payload:
|
||||||
|
| IssuesEvent
|
||||||
|
| IssueCommentEvent
|
||||||
|
| PullRequestEvent
|
||||||
|
| PullRequestReviewEvent
|
||||||
|
| PullRequestReviewCommentEvent;
|
||||||
|
entityNumber: number;
|
||||||
|
isPR: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Context for automation events (workflow_dispatch, schedule)
|
||||||
|
export type AutomationContext = BaseContext & {
|
||||||
|
eventName: AutomationEventName;
|
||||||
|
payload: WorkflowDispatchEvent | ScheduleEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Union type for all contexts
|
||||||
|
export type GitHubContext = ParsedGitHubContext | AutomationContext;
|
||||||
|
|
||||||
|
export function parseGitHubContext(): GitHubContext {
|
||||||
const context = github.context;
|
const context = github.context;
|
||||||
|
|
||||||
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
||||||
@@ -85,7 +112,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
|
|
||||||
const commonFields = {
|
const commonFields = {
|
||||||
runId: process.env.GITHUB_RUN_ID!,
|
runId: process.env.GITHUB_RUN_ID!,
|
||||||
eventName: context.eventName,
|
|
||||||
eventAction: context.payload.action,
|
eventAction: context.payload.action,
|
||||||
repository: {
|
repository: {
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -115,61 +141,67 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
|
|
||||||
switch (context.eventName) {
|
switch (context.eventName) {
|
||||||
case "issues": {
|
case "issues": {
|
||||||
|
const payload = context.payload as IssuesEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as IssuesEvent,
|
eventName: "issues",
|
||||||
entityNumber: (context.payload as IssuesEvent).issue.number,
|
payload,
|
||||||
|
entityNumber: payload.issue.number,
|
||||||
isPR: false,
|
isPR: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "issue_comment": {
|
case "issue_comment": {
|
||||||
|
const payload = context.payload as IssueCommentEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as IssueCommentEvent,
|
eventName: "issue_comment",
|
||||||
entityNumber: (context.payload as IssueCommentEvent).issue.number,
|
payload,
|
||||||
isPR: Boolean(
|
entityNumber: payload.issue.number,
|
||||||
(context.payload as IssueCommentEvent).issue.pull_request,
|
isPR: Boolean(payload.issue.pull_request),
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request": {
|
case "pull_request": {
|
||||||
|
const payload = context.payload as PullRequestEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestEvent,
|
eventName: "pull_request",
|
||||||
entityNumber: (context.payload as PullRequestEvent).pull_request.number,
|
payload,
|
||||||
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request_review": {
|
case "pull_request_review": {
|
||||||
|
const payload = context.payload as PullRequestReviewEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestReviewEvent,
|
eventName: "pull_request_review",
|
||||||
entityNumber: (context.payload as PullRequestReviewEvent).pull_request
|
payload,
|
||||||
.number,
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request_review_comment": {
|
case "pull_request_review_comment": {
|
||||||
|
const payload = context.payload as PullRequestReviewCommentEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
payload: context.payload as PullRequestReviewCommentEvent,
|
eventName: "pull_request_review_comment",
|
||||||
entityNumber: (context.payload as PullRequestReviewCommentEvent)
|
payload,
|
||||||
.pull_request.number,
|
entityNumber: payload.pull_request.number,
|
||||||
isPR: true,
|
isPR: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "workflow_dispatch": {
|
case "workflow_dispatch": {
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
payload: context.payload as unknown as WorkflowDispatchEvent,
|
payload: context.payload as unknown as WorkflowDispatchEvent,
|
||||||
// No entityNumber or isPR for workflow_dispatch
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "schedule": {
|
case "schedule": {
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
|
eventName: "schedule",
|
||||||
payload: context.payload as unknown as ScheduleEvent,
|
payload: context.payload as unknown as ScheduleEvent,
|
||||||
// No entityNumber or isPR for schedule
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -205,37 +237,53 @@ export function parseAdditionalPermissions(s: string): Map<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isIssuesEvent(
|
export function isIssuesEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
||||||
return context.eventName === "issues";
|
return context.eventName === "issues";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIssueCommentEvent(
|
export function isIssueCommentEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
||||||
return context.eventName === "issue_comment";
|
return context.eventName === "issue_comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestEvent(
|
export function isPullRequestEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
||||||
return context.eventName === "pull_request";
|
return context.eventName === "pull_request";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestReviewEvent(
|
export function isPullRequestReviewEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
||||||
return context.eventName === "pull_request_review";
|
return context.eventName === "pull_request_review";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPullRequestReviewCommentEvent(
|
export function isPullRequestReviewCommentEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
||||||
return context.eventName === "pull_request_review_comment";
|
return context.eventName === "pull_request_review_comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIssuesAssignedEvent(
|
export function isIssuesAssignedEvent(
|
||||||
context: ParsedGitHubContext,
|
context: GitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
||||||
return isIssuesEvent(context) && context.eventAction === "assigned";
|
return isIssuesEvent(context) && context.eventAction === "assigned";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type guard to check if context is an entity context (has entityNumber and isPR)
|
||||||
|
export function isEntityContext(
|
||||||
|
context: GitHubContext,
|
||||||
|
): context is ParsedGitHubContext {
|
||||||
|
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to check if context is an automation context
|
||||||
|
export function isAutomationContext(
|
||||||
|
context: GitHubContext,
|
||||||
|
): context is AutomationContext {
|
||||||
|
return AUTOMATION_EVENT_NAMES.includes(
|
||||||
|
context.eventName as AutomationEventName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ 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);
|
||||||
|
|
||||||
@@ -42,7 +36,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: entityNumber,
|
issue_number: context.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;
|
||||||
@@ -65,7 +59,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.issues.createComment({
|
response = await octokit.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: entityNumber,
|
issue_number: context.entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,7 +68,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.pulls.createReplyForReviewComment({
|
response = await octokit.rest.pulls.createReplyForReviewComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number: entityNumber,
|
pull_number: context.entityNumber,
|
||||||
comment_id: context.payload.comment.id,
|
comment_id: context.payload.comment.id,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
@@ -83,7 +77,7 @@ export async function createInitialComment(
|
|||||||
response = await octokit.rest.issues.createComment({
|
response = await octokit.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: entityNumber,
|
issue_number: context.entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,7 +95,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: entityNumber,
|
issue_number: context.entityNumber,
|
||||||
body: initialBody,
|
body: initialBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { mkdir, writeFile } from "fs/promises";
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||||
|
import { isAutomationContext } from "../../github/context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent mode implementation.
|
* Agent mode implementation.
|
||||||
@@ -15,10 +16,7 @@ export const agentMode: Mode = {
|
|||||||
|
|
||||||
shouldTrigger(context) {
|
shouldTrigger(context) {
|
||||||
// Only trigger for automation events
|
// Only trigger for automation events
|
||||||
return (
|
return isAutomationContext(context);
|
||||||
context.eventName === "workflow_dispatch" ||
|
|
||||||
context.eventName === "schedule"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
prepareContext(context) {
|
prepareContext(context) {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
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";
|
import type { GitHubContext } from "../github/context";
|
||||||
|
import { isAutomationContext } 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;
|
||||||
@@ -34,7 +35,7 @@ const modes = {
|
|||||||
* @returns The requested mode
|
* @returns The requested mode
|
||||||
* @throws Error if the mode is not found or cannot handle the event
|
* @throws Error if the mode is not found or cannot handle the event
|
||||||
*/
|
*/
|
||||||
export function getMode(name: ModeName, context: ParsedGitHubContext): Mode {
|
export function getMode(name: ModeName, context: GitHubContext): Mode {
|
||||||
const mode = modes[name];
|
const mode = modes[name];
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
const validModes = VALID_MODES.join("', '");
|
const validModes = VALID_MODES.join("', '");
|
||||||
@@ -44,11 +45,7 @@ export function getMode(name: ModeName, context: ParsedGitHubContext): Mode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate mode can handle the event type
|
// Validate mode can handle the event type
|
||||||
if (
|
if (name === "tag" && isAutomationContext(context)) {
|
||||||
name === "tag" &&
|
|
||||||
(context.eventName === "workflow_dispatch" ||
|
|
||||||
context.eventName === "schedule")
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
|
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { configureGitAuth } from "../../github/operations/git-config";
|
|||||||
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||||
import { fetchGitHubData } from "../../github/data/fetcher";
|
import { fetchGitHubData } from "../../github/data/fetcher";
|
||||||
import { createPrompt } from "../../create-prompt";
|
import { createPrompt } from "../../create-prompt";
|
||||||
|
import { isEntityContext } from "../../github/context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag mode implementation.
|
* Tag mode implementation.
|
||||||
@@ -21,6 +22,10 @@ export const tagMode: Mode = {
|
|||||||
description: "Traditional implementation mode triggered by @claude mentions",
|
description: "Traditional implementation mode triggered by @claude mentions",
|
||||||
|
|
||||||
shouldTrigger(context) {
|
shouldTrigger(context) {
|
||||||
|
// Tag mode only handles entity events
|
||||||
|
if (!isEntityContext(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return checkContainsTrigger(context);
|
return checkContainsTrigger(context);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,7 +56,10 @@ export const tagMode: Mode = {
|
|||||||
octokit,
|
octokit,
|
||||||
githubToken,
|
githubToken,
|
||||||
}: ModeOptions): Promise<ModeResult> {
|
}: ModeOptions): Promise<ModeResult> {
|
||||||
// Tag mode handles entity-based events (issues, PRs, comments)
|
// Tag mode only handles entity-based events
|
||||||
|
if (!isEntityContext(context)) {
|
||||||
|
throw new Error("Tag mode requires entity context");
|
||||||
|
}
|
||||||
|
|
||||||
// Check if actor is human
|
// Check if actor is human
|
||||||
await checkHumanActor(octokit.rest, context);
|
await checkHumanActor(octokit.rest, context);
|
||||||
@@ -60,11 +68,6 @@ export const tagMode: Mode = {
|
|||||||
const commentData = await createInitialComment(octokit.rest, context);
|
const commentData = await createInitialComment(octokit.rest, context);
|
||||||
const commentId = commentData.id;
|
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({
|
const githubData = await fetchGitHubData({
|
||||||
octokits: octokit,
|
octokits: octokit,
|
||||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { ParsedGitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
|
|
||||||
export type ModeName = "tag" | "agent";
|
export type ModeName = "tag" | "agent";
|
||||||
|
|
||||||
export type ModeContext = {
|
export type ModeContext = {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
githubContext: ParsedGitHubContext;
|
githubContext: GitHubContext;
|
||||||
commentId?: number;
|
commentId?: number;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
@@ -32,12 +32,12 @@ export type Mode = {
|
|||||||
/**
|
/**
|
||||||
* Determines if this mode should trigger based on the GitHub context
|
* Determines if this mode should trigger based on the GitHub context
|
||||||
*/
|
*/
|
||||||
shouldTrigger(context: ParsedGitHubContext): boolean;
|
shouldTrigger(context: GitHubContext): boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the mode context with any additional data needed for prompt generation
|
* Prepares the mode context with any additional data needed for prompt generation
|
||||||
*/
|
*/
|
||||||
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
|
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of tools that should be allowed for this mode
|
* Returns the list of tools that should be allowed for this mode
|
||||||
@@ -64,7 +64,7 @@ export type Mode = {
|
|||||||
|
|
||||||
// Define types for mode prepare method to avoid circular dependencies
|
// Define types for mode prepare method to avoid circular dependencies
|
||||||
export type ModeOptions = {
|
export type ModeOptions = {
|
||||||
context: ParsedGitHubContext;
|
context: GitHubContext;
|
||||||
octokit: any; // We'll use any to avoid circular dependency with Octokits
|
octokit: any; // We'll use any to avoid circular dependency with Octokits
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ParsedGitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
import type { Octokits } from "../github/api/client";
|
import type { Octokits } from "../github/api/client";
|
||||||
import type { Mode } from "../modes/types";
|
import type { Mode } from "../modes/types";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export type PrepareResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PrepareOptions = {
|
export type PrepareOptions = {
|
||||||
context: ParsedGitHubContext;
|
context: GitHubContext;
|
||||||
octokit: Octokits;
|
octokit: Octokits;
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type {
|
||||||
|
ParsedGitHubContext,
|
||||||
|
AutomationContext,
|
||||||
|
} from "../src/github/context";
|
||||||
import type {
|
import type {
|
||||||
IssuesEvent,
|
IssuesEvent,
|
||||||
IssueCommentEvent,
|
IssueCommentEvent,
|
||||||
@@ -38,7 +41,7 @@ export const createMockContext = (
|
|||||||
): ParsedGitHubContext => {
|
): ParsedGitHubContext => {
|
||||||
const baseContext: ParsedGitHubContext = {
|
const baseContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
eventName: "",
|
eventName: "issue_comment", // Default to a valid entity event
|
||||||
eventAction: "",
|
eventAction: "",
|
||||||
repository: defaultRepository,
|
repository: defaultRepository,
|
||||||
actor: "test-actor",
|
actor: "test-actor",
|
||||||
@@ -55,6 +58,22 @@ export const createMockContext = (
|
|||||||
return { ...baseContext, ...overrides };
|
return { ...baseContext, ...overrides };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMockAutomationContext = (
|
||||||
|
overrides: Partial<AutomationContext> = {},
|
||||||
|
): AutomationContext => {
|
||||||
|
const baseContext: AutomationContext = {
|
||||||
|
runId: "1234567890",
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
eventAction: undefined,
|
||||||
|
repository: defaultRepository,
|
||||||
|
actor: "test-actor",
|
||||||
|
payload: {} as any,
|
||||||
|
inputs: defaultInputs,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...baseContext, ...overrides };
|
||||||
|
};
|
||||||
|
|
||||||
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
eventName: "issues",
|
eventName: "issues",
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { describe, test, expect, beforeEach } from "bun:test";
|
import { describe, test, expect, beforeEach } from "bun:test";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import type { ParsedGitHubContext } from "../../src/github/context";
|
import type { GitHubContext } from "../../src/github/context";
|
||||||
import { createMockContext } from "../mockContext";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: ParsedGitHubContext;
|
let mockContext: GitHubContext;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockContext({
|
mockContext = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
isPR: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,30 +33,26 @@ describe("Agent Mode", () => {
|
|||||||
|
|
||||||
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
|
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
|
||||||
// Should trigger for automation events
|
// Should trigger for automation events
|
||||||
const workflowDispatchContext = createMockContext({
|
const workflowDispatchContext = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
isPR: false,
|
|
||||||
});
|
});
|
||||||
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
|
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
|
||||||
|
|
||||||
const scheduleContext = createMockContext({
|
const scheduleContext = createMockAutomationContext({
|
||||||
eventName: "schedule",
|
eventName: "schedule",
|
||||||
isPR: false,
|
|
||||||
});
|
});
|
||||||
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
|
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
|
||||||
|
|
||||||
// Should NOT trigger for other events
|
// Should NOT trigger for entity events
|
||||||
const otherEvents = [
|
const entityEvents = [
|
||||||
"push",
|
|
||||||
"repository_dispatch",
|
|
||||||
"issue_comment",
|
"issue_comment",
|
||||||
"pull_request",
|
"pull_request",
|
||||||
"pull_request_review",
|
"pull_request_review",
|
||||||
"issues",
|
"issues",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
otherEvents.forEach((eventName) => {
|
entityEvents.forEach((eventName) => {
|
||||||
const context = createMockContext({ eventName, isPR: false });
|
const context = createMockContext({ eventName });
|
||||||
expect(agentMode.shouldTrigger(context)).toBe(false);
|
expect(agentMode.shouldTrigger(context)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ 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";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
|
|
||||||
describe("Mode Registry", () => {
|
describe("Mode Registry", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
eventName: "issue_comment",
|
eventName: "issue_comment",
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockWorkflowDispatchContext = createMockContext({
|
const mockWorkflowDispatchContext = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockScheduleContext = createMockContext({
|
const mockScheduleContext = createMockAutomationContext({
|
||||||
eventName: "schedule",
|
eventName: "schedule",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -299,15 +299,4 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
expect(result.allowedTools).toBe("Tool1,Tool2");
|
expect(result.allowedTools).toBe("Tool1,Tool2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error for unsupported event type", () => {
|
|
||||||
process.env = BASE_ENV;
|
|
||||||
const unsupportedContext = createMockContext({
|
|
||||||
eventName: "unsupported_event",
|
|
||||||
eventAction: "whatever",
|
|
||||||
});
|
|
||||||
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
|
|
||||||
"Unsupported event type: unsupported_event",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -474,17 +474,6 @@ describe("checkContainsTrigger", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("non-matching events", () => {
|
|
||||||
it("should return false for non-matching event type", () => {
|
|
||||||
const context = createMockContext({
|
|
||||||
eventName: "push",
|
|
||||||
eventAction: "created",
|
|
||||||
payload: {} as any,
|
|
||||||
});
|
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("escapeRegExp", () => {
|
describe("escapeRegExp", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user