mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
add schedule & workflow dispatch paths. Also make prepare logic conditional
This commit is contained in:
49
examples/workflow-dispatch-agent.yml
Normal file
49
examples/workflow-dispatch-agent.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Claude Automation Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
task:
|
||||||
|
description: "Task for Claude to perform"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: "Update dependencies and run tests"
|
||||||
|
branch:
|
||||||
|
description: "Branch to work on"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "main"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.branch || 'main' }}
|
||||||
|
|
||||||
|
- name: Run Claude Code in Agent Mode
|
||||||
|
uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
mode: agent
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
override_prompt: |
|
||||||
|
You are running in an automated workflow context.
|
||||||
|
|
||||||
|
Task: ${{ github.event.inputs.task }}
|
||||||
|
|
||||||
|
Current branch: ${{ github.event.inputs.branch || 'main' }}
|
||||||
|
|
||||||
|
Please complete the requested task. Remember:
|
||||||
|
- You have full access to the repository
|
||||||
|
- You can make commits and push changes
|
||||||
|
- Focus on the specific task provided
|
||||||
|
- Provide clear feedback about what you're doing
|
||||||
@@ -34,6 +34,16 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
|
|
||||||
|
function getEntityNumberXml(eventData: EventData): string {
|
||||||
|
if (eventData.isPR && "prNumber" in eventData) {
|
||||||
|
return `<pr_number>${eventData.prNumber}</pr_number>`;
|
||||||
|
}
|
||||||
|
if ("issueNumber" in eventData) {
|
||||||
|
return `<issue_number>${eventData.issueNumber}</issue_number>`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAllowedToolsString(
|
export function buildAllowedToolsString(
|
||||||
customAllowedTools?: string[],
|
customAllowedTools?: string[],
|
||||||
includeActionsTools: boolean = false,
|
includeActionsTools: boolean = false,
|
||||||
@@ -125,8 +135,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;
|
||||||
@@ -338,6 +350,24 @@ export function prepareContext(
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "workflow_dispatch":
|
||||||
|
eventData = {
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
isPR: false,
|
||||||
|
...(baseBranch && { baseBranch }),
|
||||||
|
...(claudeBranch && { claudeBranch }),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "schedule":
|
||||||
|
eventData = {
|
||||||
|
eventName: "schedule",
|
||||||
|
isPR: false,
|
||||||
|
...(baseBranch && { baseBranch }),
|
||||||
|
...(claudeBranch && { claudeBranch }),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported event type: ${eventName}`);
|
throw new Error(`Unsupported event type: ${eventName}`);
|
||||||
}
|
}
|
||||||
@@ -400,6 +430,18 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
: `pull request event`,
|
: `pull request event`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case "workflow_dispatch":
|
||||||
|
return {
|
||||||
|
eventType: "WORKFLOW_DISPATCH",
|
||||||
|
triggerContext: `workflow dispatch event`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "schedule":
|
||||||
|
return {
|
||||||
|
eventType: "SCHEDULE",
|
||||||
|
triggerContext: `scheduled automation event`,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected event type`);
|
throw new Error(`Unexpected event type`);
|
||||||
}
|
}
|
||||||
@@ -407,11 +449,12 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
|
|
||||||
function getCommitInstructions(
|
function getCommitInstructions(
|
||||||
eventData: EventData,
|
eventData: EventData,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult | null,
|
||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
useCommitSigning: boolean,
|
useCommitSigning: boolean,
|
||||||
): string {
|
): string {
|
||||||
const coAuthorLine =
|
const coAuthorLine =
|
||||||
|
githubData &&
|
||||||
(githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown")
|
(githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown")
|
||||||
? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>`
|
? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>`
|
||||||
: "";
|
: "";
|
||||||
@@ -466,8 +509,26 @@ function getCommitInstructions(
|
|||||||
function substitutePromptVariables(
|
function substitutePromptVariables(
|
||||||
template: string,
|
template: string,
|
||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult | null,
|
||||||
): string {
|
): string {
|
||||||
|
// Handle automation events without GitHub data
|
||||||
|
if (!githubData) {
|
||||||
|
const { eventData } = context;
|
||||||
|
const variables: Record<string, string> = {
|
||||||
|
EVENT_TYPE: eventData.eventName,
|
||||||
|
REPOSITORY: context.repository,
|
||||||
|
TRIGGER_USERNAME: context.triggerUsername ?? "automation",
|
||||||
|
CURRENT_BRANCH: eventData.claudeBranch || eventData.baseBranch || "main",
|
||||||
|
BASE_BRANCH: eventData.baseBranch || "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(variables).reduce(
|
||||||
|
(prompt, [key, value]) =>
|
||||||
|
prompt.replace(new RegExp(`{{${key}}}`, "g"), value),
|
||||||
|
template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { contextData, comments, reviewData, changedFilesWithSHA } = githubData;
|
const { contextData, comments, reviewData, changedFilesWithSHA } = githubData;
|
||||||
const { eventData } = context;
|
const { eventData } = context;
|
||||||
|
|
||||||
@@ -528,7 +589,7 @@ function substitutePromptVariables(
|
|||||||
|
|
||||||
export function generatePrompt(
|
export function generatePrompt(
|
||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult | null,
|
||||||
useCommitSigning: boolean,
|
useCommitSigning: boolean,
|
||||||
): string {
|
): string {
|
||||||
if (context.overridePrompt) {
|
if (context.overridePrompt) {
|
||||||
@@ -539,6 +600,59 @@ export function generatePrompt(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { eventData } = context;
|
||||||
|
|
||||||
|
// Handle automation events that don't have GitHub data
|
||||||
|
if (!githubData) {
|
||||||
|
// For automation events, we have minimal context
|
||||||
|
const { eventType, triggerContext } = getEventTypeAndContext(context);
|
||||||
|
|
||||||
|
let promptContent = `You are Claude, an AI assistant designed to help with GitHub ${eventData.eventName === "workflow_dispatch" ? "workflow dispatch" : "scheduled automation"} tasks. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task:
|
||||||
|
|
||||||
|
<formatted_context>
|
||||||
|
Repository: ${context.repository}
|
||||||
|
Event Type: ${eventData.eventName}
|
||||||
|
Current Branch: ${eventData.claudeBranch || eventData.baseBranch || "main"}
|
||||||
|
Base Branch: ${eventData.baseBranch || "main"}
|
||||||
|
Actor: ${context.triggerUsername ?? "automation"}
|
||||||
|
</formatted_context>
|
||||||
|
|
||||||
|
<event_type>${eventType}</event_type>
|
||||||
|
<is_pr>false</is_pr>
|
||||||
|
<trigger_context>${triggerContext}</trigger_context>
|
||||||
|
<repository>${context.repository}</repository>
|
||||||
|
<trigger_username>${context.triggerUsername ?? "automation"}</trigger_username>
|
||||||
|
${
|
||||||
|
context.directPrompt
|
||||||
|
? `<direct_prompt>
|
||||||
|
IMPORTANT: The following are direct instructions from the automation workflow:
|
||||||
|
|
||||||
|
${sanitizeContent(context.directPrompt)}
|
||||||
|
</direct_prompt>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
You have been triggered by an automated workflow. Follow these guidelines:
|
||||||
|
|
||||||
|
1. **Context**: You are running in an automated context without a specific issue or PR.
|
||||||
|
2. **Branch**: You are currently on the ${eventData.claudeBranch || eventData.baseBranch || "main"} branch.
|
||||||
|
3. **Tools**: You have access to file system and Git tools to make changes.
|
||||||
|
${
|
||||||
|
useCommitSigning
|
||||||
|
? "4. **Commits**: Use the MCP file operations server for signed commits."
|
||||||
|
: "4. **Commits**: You can commit changes directly using Git."
|
||||||
|
}
|
||||||
|
5. **Scope**: Focus on the task described${context.directPrompt ? " in the direct_prompt above" : ""}.
|
||||||
|
|
||||||
|
Please proceed with the automated task.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
${context.customInstructions || ""}`;
|
||||||
|
|
||||||
|
return promptContent;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
contextData,
|
contextData,
|
||||||
comments,
|
comments,
|
||||||
@@ -546,11 +660,10 @@ export function generatePrompt(
|
|||||||
reviewData,
|
reviewData,
|
||||||
imageUrlMap,
|
imageUrlMap,
|
||||||
} = githubData;
|
} = githubData;
|
||||||
const { eventData } = context;
|
|
||||||
|
|
||||||
const { eventType, triggerContext } = getEventTypeAndContext(context);
|
const { eventType, triggerContext } = getEventTypeAndContext(context);
|
||||||
|
|
||||||
const formattedContext = formatContext(contextData, eventData.isPR);
|
const formattedContext = formatContext(contextData, eventData.isPR ?? false);
|
||||||
const formattedComments = formatComments(comments, imageUrlMap);
|
const formattedComments = formatComments(comments, imageUrlMap);
|
||||||
const formattedReviewComments = eventData.isPR
|
const formattedReviewComments = eventData.isPR
|
||||||
? formatReviewComments(reviewData, imageUrlMap)
|
? formatReviewComments(reviewData, imageUrlMap)
|
||||||
@@ -596,17 +709,13 @@ ${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
|
|||||||
</changed_files>${imagesInfo}
|
</changed_files>${imagesInfo}
|
||||||
|
|
||||||
<event_type>${eventType}</event_type>
|
<event_type>${eventType}</event_type>
|
||||||
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
|
<is_pr>${(eventData.isPR ?? false) ? "true" : "false"}</is_pr>
|
||||||
<trigger_context>${triggerContext}</trigger_context>
|
<trigger_context>${triggerContext}</trigger_context>
|
||||||
<repository>${context.repository}</repository>
|
<repository>${context.repository}</repository>
|
||||||
${
|
${getEntityNumberXml(eventData)}
|
||||||
eventData.isPR
|
|
||||||
? `<pr_number>${eventData.prNumber}</pr_number>`
|
|
||||||
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
|
|
||||||
}
|
|
||||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||||
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
<trigger_display_name>${githubData?.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
||||||
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
||||||
${
|
${
|
||||||
(eventData.eventName === "issue_comment" ||
|
(eventData.eventName === "issue_comment" ||
|
||||||
@@ -797,7 +906,7 @@ f. If you are unable to complete certain steps, such as running a linter or test
|
|||||||
export async function createPrompt(
|
export async function createPrompt(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
modeContext: ModeContext,
|
modeContext: ModeContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult | null,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -88,6 +88,20 @@ type PullRequestEvent = {
|
|||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WorkflowDispatchEvent = {
|
||||||
|
eventName: "workflow_dispatch";
|
||||||
|
isPR?: false;
|
||||||
|
baseBranch?: string;
|
||||||
|
claudeBranch?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScheduleEvent = {
|
||||||
|
eventName: "schedule";
|
||||||
|
isPR?: false;
|
||||||
|
baseBranch?: string;
|
||||||
|
claudeBranch?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Union type for all possible event types
|
// Union type for all possible event types
|
||||||
export type EventData =
|
export type EventData =
|
||||||
| PullRequestReviewCommentEvent
|
| PullRequestReviewCommentEvent
|
||||||
@@ -97,7 +111,9 @@ export type EventData =
|
|||||||
| IssueOpenedEvent
|
| IssueOpenedEvent
|
||||||
| IssueAssignedEvent
|
| IssueAssignedEvent
|
||||||
| IssueLabeledEvent
|
| IssueLabeledEvent
|
||||||
| PullRequestEvent;
|
| PullRequestEvent
|
||||||
|
| WorkflowDispatchEvent
|
||||||
|
| ScheduleEvent;
|
||||||
|
|
||||||
// Combined type with separate eventData field
|
// Combined type with separate eventData field
|
||||||
export type PreparedContext = CommonFields & {
|
export type PreparedContext = CommonFields & {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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 entityNumber");
|
||||||
|
}
|
||||||
|
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,34 @@ 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 +53,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 +159,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;
|
||||||
|
|
||||||
|
// Entity events should always have entityNumber
|
||||||
|
if (!context.entityNumber) {
|
||||||
|
throw new Error("createInitialComment requires an entityNumber");
|
||||||
|
}
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
src/github/operations/default-branch.ts
Normal file
13
src/github/operations/default-branch.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
|
export async function getDefaultBranch(
|
||||||
|
rest: Octokit,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const repoResponse = await rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
return repoResponse.data.default_branch;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
70
src/prepare/automation-events.ts
Normal file
70
src/prepare/automation-events.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Prepare logic for automation events (workflow_dispatch, schedule)
|
||||||
|
* These events don't have associated GitHub entities and require minimal setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { prepareMcpConfig } from "../mcp/install-mcp-server";
|
||||||
|
import { createPrompt } from "../create-prompt";
|
||||||
|
import { getDefaultBranch } from "../github/operations/default-branch";
|
||||||
|
import type { PrepareOptions, PrepareResult } from "./types";
|
||||||
|
|
||||||
|
export async function prepareAutomationEvent({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
mode,
|
||||||
|
githubToken,
|
||||||
|
}: PrepareOptions): Promise<PrepareResult> {
|
||||||
|
// For automation events, we skip:
|
||||||
|
// - Human actor check (it's automation)
|
||||||
|
// - Tracking comment (no issue/PR to comment on)
|
||||||
|
// - GitHub data fetching (no entity to fetch)
|
||||||
|
// - Branch setup (use default branch or current branch)
|
||||||
|
|
||||||
|
// Get the default branch or use the one specified in inputs
|
||||||
|
const baseBranch =
|
||||||
|
context.inputs.baseBranch ||
|
||||||
|
(await getDefaultBranch(
|
||||||
|
octokit.rest,
|
||||||
|
context.repository.owner,
|
||||||
|
context.repository.repo,
|
||||||
|
));
|
||||||
|
|
||||||
|
// For automation events, we stay on the current branch (typically main/master)
|
||||||
|
const branchInfo = {
|
||||||
|
baseBranch,
|
||||||
|
currentBranch: baseBranch,
|
||||||
|
claudeBranch: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create prompt file with minimal context
|
||||||
|
const modeContext = mode.prepareContext(context, {
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
claudeBranch: branchInfo.claudeBranch,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass null for githubData since automation events don't have associated entities
|
||||||
|
await createPrompt(mode, modeContext, null, context);
|
||||||
|
|
||||||
|
// Get MCP configuration
|
||||||
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
|
const mcpConfig = await prepareMcpConfig({
|
||||||
|
githubToken,
|
||||||
|
owner: context.repository.owner,
|
||||||
|
repo: context.repository.repo,
|
||||||
|
branch: branchInfo.currentBranch,
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
additionalMcpConfig,
|
||||||
|
claudeCommentId: "",
|
||||||
|
allowedTools: context.inputs.allowedTools,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentId: undefined,
|
||||||
|
branchInfo,
|
||||||
|
mcpConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
89
src/prepare/entity-events.ts
Normal file
89
src/prepare/entity-events.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Prepare logic for entity-based events (issues, PRs, comments)
|
||||||
|
* These events have associated GitHub entities that need to be fetched and managed
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { checkHumanActor } from "../github/validation/actor";
|
||||||
|
import { createInitialComment } from "../github/operations/comments/create-initial";
|
||||||
|
import { setupBranch } from "../github/operations/branch";
|
||||||
|
import { configureGitAuth } from "../github/operations/git-config";
|
||||||
|
import { prepareMcpConfig } from "../mcp/install-mcp-server";
|
||||||
|
import { fetchGitHubData } from "../github/data/fetcher";
|
||||||
|
import { createPrompt } from "../create-prompt";
|
||||||
|
import type { PrepareOptions, PrepareResult } from "./types";
|
||||||
|
|
||||||
|
export async function prepareEntityEvent({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
mode,
|
||||||
|
githubToken,
|
||||||
|
}: PrepareOptions): Promise<PrepareResult> {
|
||||||
|
// Check if actor is human
|
||||||
|
await checkHumanActor(octokit.rest, context);
|
||||||
|
|
||||||
|
// Create initial tracking comment (mode-aware)
|
||||||
|
let commentId: number | undefined;
|
||||||
|
let commentData: Awaited<ReturnType<typeof createInitialComment>> | undefined;
|
||||||
|
if (mode.shouldCreateTrackingComment()) {
|
||||||
|
commentData = await createInitialComment(octokit.rest, context);
|
||||||
|
commentId = commentData.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch GitHub data - entity events always have entityNumber and isPR
|
||||||
|
if (!context.entityNumber || context.isPR === undefined) {
|
||||||
|
throw new Error("Entity events must have entityNumber and isPR defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubData = await fetchGitHubData({
|
||||||
|
octokits: octokit,
|
||||||
|
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||||
|
prNumber: context.entityNumber.toString(),
|
||||||
|
isPR: context.isPR,
|
||||||
|
triggerUsername: context.actor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup branch
|
||||||
|
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||||
|
|
||||||
|
// Configure git authentication if not using commit signing
|
||||||
|
if (!context.inputs.useCommitSigning) {
|
||||||
|
try {
|
||||||
|
await configureGitAuth(githubToken, context, commentData?.user || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to configure git authentication:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create prompt file
|
||||||
|
const modeContext = mode.prepareContext(context, {
|
||||||
|
commentId,
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
claudeBranch: branchInfo.claudeBranch,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPrompt(mode, modeContext, githubData, context);
|
||||||
|
|
||||||
|
// Get MCP configuration
|
||||||
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
|
const mcpConfig = await prepareMcpConfig({
|
||||||
|
githubToken,
|
||||||
|
owner: context.repository.owner,
|
||||||
|
repo: context.repository.repo,
|
||||||
|
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||||
|
baseBranch: branchInfo.baseBranch,
|
||||||
|
additionalMcpConfig,
|
||||||
|
claudeCommentId: commentId?.toString() || "",
|
||||||
|
allowedTools: context.inputs.allowedTools,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentId,
|
||||||
|
branchInfo,
|
||||||
|
mcpConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
21
src/prepare/index.ts
Normal file
21
src/prepare/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Main prepare module that routes to appropriate prepare logic based on event type
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PrepareOptions, PrepareResult } from "./types";
|
||||||
|
import { prepareEntityEvent } from "./entity-events";
|
||||||
|
import { prepareAutomationEvent } from "./automation-events";
|
||||||
|
|
||||||
|
const AUTOMATION_EVENTS = ["workflow_dispatch", "schedule"];
|
||||||
|
|
||||||
|
export async function prepare(options: PrepareOptions): Promise<PrepareResult> {
|
||||||
|
const { context } = options;
|
||||||
|
|
||||||
|
if (AUTOMATION_EVENTS.includes(context.eventName)) {
|
||||||
|
console.log(`Preparing automation event: ${context.eventName}`);
|
||||||
|
return prepareAutomationEvent(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Preparing entity-based event: ${context.eventName}`);
|
||||||
|
return prepareEntityEvent(options);
|
||||||
|
}
|
||||||
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user