mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54: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"];
|
||||
|
||||
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(
|
||||
customAllowedTools?: string[],
|
||||
includeActionsTools: boolean = false,
|
||||
@@ -125,8 +135,10 @@ export function prepareContext(
|
||||
const isPR = context.isPR;
|
||||
|
||||
// Get PR/Issue number from entityNumber
|
||||
const prNumber = isPR ? context.entityNumber.toString() : undefined;
|
||||
const issueNumber = !isPR ? context.entityNumber.toString() : undefined;
|
||||
const prNumber =
|
||||
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
|
||||
let triggerUsername: string | undefined;
|
||||
@@ -338,6 +350,24 @@ export function prepareContext(
|
||||
};
|
||||
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:
|
||||
throw new Error(`Unsupported event type: ${eventName}`);
|
||||
}
|
||||
@@ -400,6 +430,18 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
: `pull request event`,
|
||||
};
|
||||
|
||||
case "workflow_dispatch":
|
||||
return {
|
||||
eventType: "WORKFLOW_DISPATCH",
|
||||
triggerContext: `workflow dispatch event`,
|
||||
};
|
||||
|
||||
case "schedule":
|
||||
return {
|
||||
eventType: "SCHEDULE",
|
||||
triggerContext: `scheduled automation event`,
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected event type`);
|
||||
}
|
||||
@@ -407,11 +449,12 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
|
||||
function getCommitInstructions(
|
||||
eventData: EventData,
|
||||
githubData: FetchDataResult,
|
||||
githubData: FetchDataResult | null,
|
||||
context: PreparedContext,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
const coAuthorLine =
|
||||
githubData &&
|
||||
(githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown")
|
||||
? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>`
|
||||
: "";
|
||||
@@ -466,8 +509,26 @@ function getCommitInstructions(
|
||||
function substitutePromptVariables(
|
||||
template: string,
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
githubData: FetchDataResult | null,
|
||||
): 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 { eventData } = context;
|
||||
|
||||
@@ -528,7 +589,7 @@ function substitutePromptVariables(
|
||||
|
||||
export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
githubData: FetchDataResult | null,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
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 {
|
||||
contextData,
|
||||
comments,
|
||||
@@ -546,11 +660,10 @@ export function generatePrompt(
|
||||
reviewData,
|
||||
imageUrlMap,
|
||||
} = githubData;
|
||||
const { eventData } = 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 formattedReviewComments = eventData.isPR
|
||||
? formatReviewComments(reviewData, imageUrlMap)
|
||||
@@ -596,17 +709,13 @@ ${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
|
||||
</changed_files>${imagesInfo}
|
||||
|
||||
<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>
|
||||
<repository>${context.repository}</repository>
|
||||
${
|
||||
eventData.isPR
|
||||
? `<pr_number>${eventData.prNumber}</pr_number>`
|
||||
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
|
||||
}
|
||||
${getEntityNumberXml(eventData)}
|
||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||
<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>
|
||||
${
|
||||
(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(
|
||||
mode: Mode,
|
||||
modeContext: ModeContext,
|
||||
githubData: FetchDataResult,
|
||||
githubData: FetchDataResult | null,
|
||||
context: ParsedGitHubContext,
|
||||
) {
|
||||
try {
|
||||
|
||||
@@ -88,6 +88,20 @@ type PullRequestEvent = {
|
||||
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
|
||||
export type EventData =
|
||||
| PullRequestReviewCommentEvent
|
||||
@@ -97,7 +111,9 @@ export type EventData =
|
||||
| IssueOpenedEvent
|
||||
| IssueAssignedEvent
|
||||
| IssueLabeledEvent
|
||||
| PullRequestEvent;
|
||||
| PullRequestEvent
|
||||
| WorkflowDispatchEvent
|
||||
| ScheduleEvent;
|
||||
|
||||
// Combined type with separate eventData field
|
||||
export type PreparedContext = CommonFields & {
|
||||
|
||||
@@ -7,17 +7,11 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { setupGitHubToken } from "../github/token";
|
||||
import { checkHumanActor } from "../github/validation/actor";
|
||||
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 { fetchGitHubData } from "../github/data/fetcher";
|
||||
import { parseGitHubContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { createPrompt } from "../create-prompt";
|
||||
import { prepare } from "../prepare";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
@@ -51,65 +45,16 @@ async function run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Check if actor is human
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// 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,
|
||||
// Step 5: Use the new modular prepare function
|
||||
const result = await prepare({
|
||||
context,
|
||||
octokit,
|
||||
mode,
|
||||
githubToken,
|
||||
});
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
|
||||
// Set the MCP config output
|
||||
core.setOutput("mcp_config", result.mcpConfig);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||
|
||||
@@ -24,6 +24,13 @@ async function run() {
|
||||
|
||||
const context = parseGitHubContext();
|
||||
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 serverUrl = GITHUB_SERVER_URL;
|
||||
@@ -73,7 +80,7 @@ async function run() {
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: context.entityNumber,
|
||||
pull_number: entityNumber,
|
||||
});
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR comments count: ${pr.comments}`);
|
||||
|
||||
@@ -7,6 +7,34 @@ import type {
|
||||
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";
|
||||
|
||||
@@ -25,9 +53,11 @@ export type ParsedGitHubContext = {
|
||||
| IssueCommentEvent
|
||||
| PullRequestEvent
|
||||
| PullRequestReviewEvent
|
||||
| PullRequestReviewCommentEvent;
|
||||
entityNumber: number;
|
||||
isPR: boolean;
|
||||
| PullRequestReviewCommentEvent
|
||||
| WorkflowDispatchEvent
|
||||
| ScheduleEvent;
|
||||
entityNumber?: number;
|
||||
isPR?: boolean;
|
||||
inputs: {
|
||||
mode: ModeName;
|
||||
triggerPhrase: string;
|
||||
@@ -129,6 +159,20 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ export async function createInitialComment(
|
||||
) {
|
||||
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 initialBody = createCommentBody(jobRunLink);
|
||||
|
||||
@@ -36,7 +42,7 @@ export async function createInitialComment(
|
||||
const comments = await octokit.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
issue_number: entityNumber,
|
||||
});
|
||||
const existingComment = comments.data.find((comment) => {
|
||||
const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID;
|
||||
@@ -59,7 +65,7 @@ export async function createInitialComment(
|
||||
response = await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
issue_number: entityNumber,
|
||||
body: initialBody,
|
||||
});
|
||||
}
|
||||
@@ -68,7 +74,7 @@ export async function createInitialComment(
|
||||
response = await octokit.rest.pulls.createReplyForReviewComment({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: context.entityNumber,
|
||||
pull_number: entityNumber,
|
||||
comment_id: context.payload.comment.id,
|
||||
body: initialBody,
|
||||
});
|
||||
@@ -77,7 +83,7 @@ export async function createInitialComment(
|
||||
response = await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
issue_number: entityNumber,
|
||||
body: initialBody,
|
||||
});
|
||||
}
|
||||
@@ -95,7 +101,7 @@ export async function createInitialComment(
|
||||
const response = await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
issue_number: entityNumber,
|
||||
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,
|
||||
REPO_OWNER: owner,
|
||||
REPO_NAME: repo,
|
||||
PR_NUMBER: context.entityNumber.toString(),
|
||||
PR_NUMBER: context.entityNumber?.toString() || "",
|
||||
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