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:
km-anthropic
2025-07-29 14:58:59 -07:00
committed by GitHub
parent bdfdd1f788
commit daac7e353f
15 changed files with 166 additions and 133 deletions

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises";
import type { Mode, ModeOptions, ModeResult } from "../types";
import { isAutomationContext } from "../../github/context";
/**
* Agent mode implementation.
@@ -15,10 +16,7 @@ export const agentMode: Mode = {
shouldTrigger(context) {
// Only trigger for automation events
return (
context.eventName === "workflow_dispatch" ||
context.eventName === "schedule"
);
return isAutomationContext(context);
},
prepareContext(context) {

View File

@@ -13,7 +13,8 @@
import type { Mode, ModeName } from "./types";
import { tagMode } from "./tag";
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 VALID_MODES = ["tag", "agent"] as const;
@@ -34,7 +35,7 @@ const modes = {
* @returns The requested mode
* @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];
if (!mode) {
const validModes = VALID_MODES.join("', '");
@@ -44,11 +45,7 @@ export function getMode(name: ModeName, context: ParsedGitHubContext): Mode {
}
// Validate mode can handle the event type
if (
name === "tag" &&
(context.eventName === "workflow_dispatch" ||
context.eventName === "schedule")
) {
if (name === "tag" && isAutomationContext(context)) {
throw new Error(
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
);

View File

@@ -8,6 +8,7 @@ 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 { isEntityContext } from "../../github/context";
/**
* Tag mode implementation.
@@ -21,6 +22,10 @@ export const tagMode: Mode = {
description: "Traditional implementation mode triggered by @claude mentions",
shouldTrigger(context) {
// Tag mode only handles entity events
if (!isEntityContext(context)) {
return false;
}
return checkContainsTrigger(context);
},
@@ -51,7 +56,10 @@ export const tagMode: Mode = {
octokit,
githubToken,
}: 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
await checkHumanActor(octokit.rest, context);
@@ -60,11 +68,6 @@ export const tagMode: Mode = {
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}`,

View File

@@ -1,10 +1,10 @@
import type { ParsedGitHubContext } from "../github/context";
import type { GitHubContext } from "../github/context";
export type ModeName = "tag" | "agent";
export type ModeContext = {
mode: ModeName;
githubContext: ParsedGitHubContext;
githubContext: GitHubContext;
commentId?: number;
baseBranch?: string;
claudeBranch?: string;
@@ -32,12 +32,12 @@ export type Mode = {
/**
* 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
*/
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
/**
* 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
export type ModeOptions = {
context: ParsedGitHubContext;
context: GitHubContext;
octokit: any; // We'll use any to avoid circular dependency with Octokits
githubToken: string;
};