From 3305a16297ad111575525538f75f2a0e974688fd Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Tue, 30 Sep 2025 17:31:36 +0100 Subject: [PATCH] feat: add mode-aware gitea prepare flow --- package-lock.json | 249 ++++++++++++++++++++++- package.json | 1 + src/create-prompt/index.ts | 150 +++++++------- src/create-prompt/types.ts | 2 +- src/entrypoints/prepare.ts | 39 ++-- src/github/api/config.ts | 4 + src/github/operations/comment-logic.ts | 21 +- src/github/operations/comments/common.ts | 2 - src/github/operations/git-config.ts | 6 +- src/github/validation/trigger.ts | 21 ++ src/mcp/gitea-mcp-server.ts | 2 +- src/mcp/install-mcp-server.ts | 25 ++- src/mcp/local-git-ops-server.ts | 2 - 13 files changed, 404 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index a92835b..565bde2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { - "name": "claude-pr-action", + "name": "@anthropic-ai/claude-code-action", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "claude-pr-action", + "name": "@anthropic-ai/claude-code-action", "version": "1.0.0", "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", "@anthropic-ai/sdk": "^0.30.0", "@modelcontextprotocol/sdk": "^1.11.0", + "@octokit/rest": "^22.0.0", "@octokit/webhooks-types": "^7.6.1", "node-fetch": "^3.3.2", "zod": "^3.24.4" @@ -209,6 +210,82 @@ "node": ">= 18" } }, + "node_modules/@octokit/graphql": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.2.tgz", + "integrity": "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.4", + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", + "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", + "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", + "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.1", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", + "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz", + "integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^26.0.0" + } + }, + "node_modules/@octokit/graphql/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/openapi-types": { "version": "24.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", @@ -304,6 +381,158 @@ "node": ">= 18" } }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz", + "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.2", + "@octokit/request": "^10.0.4", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", + "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", + "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", + "license": "MIT" + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.0.tgz", + "integrity": "sha512-YuAlyjR8o5QoRSOvMHxSJzPtogkNMgeMv2mpccrvdUGeC3MKyfi/hS+KiFwyH/iRKIKyx+eIMsDjbt3p9r2GYA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.0.tgz", + "integrity": "sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", + "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.1", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", + "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz", + "integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^26.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", @@ -785,6 +1014,22 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 01d3e6f..67255da 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@actions/github": "^6.0.1", "@anthropic-ai/sdk": "^0.30.0", "@modelcontextprotocol/sdk": "^1.11.0", + "@octokit/rest": "^22.0.0", "@octokit/webhooks-types": "^7.6.1", "node-fetch": "^3.3.2", "zod": "^3.24.4" diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 3be8068..82d1f41 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -19,7 +19,7 @@ import { } from "../github/context"; import type { ParsedGitHubContext } from "../github/context"; import type { CommonFields, PreparedContext, EventData } from "./types"; -import { GITEA_SERVER_URL } from "../github/api/config"; +import type { Mode, ModeContext } from "../modes/types"; export type { CommonFields, PreparedContext } from "./types"; const BASE_ALLOWED_TOOLS = [ @@ -62,37 +62,74 @@ const BASE_ALLOWED_TOOLS = [ ]; const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"]; -export function buildAllowedToolsString( - customAllowedTools?: string[], -): string { - let baseTools = [...BASE_ALLOWED_TOOLS]; +const ACTIONS_ALLOWED_TOOLS = [ + "mcp__github_actions__get_ci_status", + "mcp__github_actions__get_workflow_run_details", + "mcp__github_actions__download_job_log", +]; - let allAllowedTools = baseTools.join(","); - if (customAllowedTools && customAllowedTools.length > 0) { - allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`; +const COMMIT_SIGNING_TOOLS = [ + "mcp__github_file_ops__commit_files", + "mcp__github_file_ops__delete_files", + "mcp__github_file_ops__update_claude_comment", +]; + +function normalizeToolList(input?: string | string[]): string[] { + if (!input) { + return []; } - return allAllowedTools; + + const tools = Array.isArray(input) ? input : input.split(","); + return tools + .map((tool) => tool.trim()) + .filter((tool): tool is string => tool.length > 0); +} + +export function buildAllowedToolsString( + customAllowedTools?: string | string[], + includeActionsReadTools = false, + useCommitSigning = false, +): string { + const allowedTools = new Set(BASE_ALLOWED_TOOLS); + + if (includeActionsReadTools) { + for (const tool of ACTIONS_ALLOWED_TOOLS) { + allowedTools.add(tool); + } + } + + if (useCommitSigning) { + for (const tool of COMMIT_SIGNING_TOOLS) { + allowedTools.add(tool); + } + } + + for (const tool of normalizeToolList(customAllowedTools)) { + allowedTools.add(tool); + } + + return Array.from(allowedTools).join(","); } export function buildDisallowedToolsString( - customDisallowedTools?: string[], - allowedTools?: string[], + customDisallowedTools?: string | string[], + allowedTools?: string | string[], ): string { let disallowedTools = [...DISALLOWED_TOOLS]; // If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list - if (allowedTools && allowedTools.length > 0) { - disallowedTools = disallowedTools.filter( - (tool) => !allowedTools.includes(tool), - ); + const allowedList = normalizeToolList(allowedTools); + if (allowedList.length > 0) { + disallowedTools = disallowedTools.filter((tool) => !allowedList.includes(tool)); } let allDisallowedTools = disallowedTools.join(","); - if (customDisallowedTools && customDisallowedTools.length > 0) { + const customList = normalizeToolList(customDisallowedTools); + if (customList.length > 0) { if (allDisallowedTools) { - allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`; + allDisallowedTools = `${allDisallowedTools},${customList.join(",")}`; } else { - allDisallowedTools = customDisallowedTools.join(","); + allDisallowedTools = customList.join(","); } } return allDisallowedTools; @@ -240,6 +277,8 @@ export function prepareContext( throw new Error( "ISSUE_NUMBER is required for issue_comment event for issues", ); + } else if (!claudeBranch) { + throw new Error("CLAUDE_BRANCH is required for issue_comment event"); } eventData = { @@ -249,7 +288,7 @@ export function prepareContext( baseBranch, issueNumber, commentBody, - ...(claudeBranch && { claudeBranch }), + claudeBranch, }; break; @@ -266,6 +305,9 @@ export function prepareContext( if (!baseBranch) { throw new Error("BASE_BRANCH is required for issues event"); } + if (!claudeBranch) { + throw new Error("CLAUDE_BRANCH is required for issues event"); + } if (eventAction === "assigned") { if (!assigneeTrigger && !directPrompt) { @@ -279,8 +321,8 @@ export function prepareContext( isPR: false, issueNumber, baseBranch, - assigneeTrigger, - ...(claudeBranch && { claudeBranch }), + ...(assigneeTrigger && { assigneeTrigger }), + claudeBranch, }; } else if (eventAction === "labeled") { if (!labelTrigger) { @@ -302,7 +344,7 @@ export function prepareContext( isPR: false, issueNumber, baseBranch, - ...(claudeBranch && { claudeBranch }), + claudeBranch, }; } else { throw new Error(`Unsupported issue action: ${eventAction}`); @@ -393,64 +435,6 @@ export function getEventTypeAndContext(envVars: PreparedContext): { } } -function getCommitInstructions( - eventData: EventData, - githubData: FetchDataResult, - context: PreparedContext, - useCommitSigning: boolean, -): string { - const coAuthorLine = - (githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown") - ? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>` - : ""; - - if (useCommitSigning) { - if (eventData.isPR && !eventData.claudeBranch) { - return ` - - Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files). - - Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files). - - When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message. - - Use: "${coAuthorLine}"`; - } else { - return ` - - You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch. - - Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files) - - Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files). - - When pushing changes and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message. - - Use: "${coAuthorLine}"`; - } - } else { - // Non-signing instructions - if (eventData.isPR && !eventData.claudeBranch) { - return ` - - Use git commands via the Bash tool to commit and push your changes: - - Stage files: Bash(git add ) - - Commit with a descriptive message: Bash(git commit -m "") - ${ - coAuthorLine - ? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer: - Bash(git commit -m "\\n\\n${coAuthorLine}")` - : "" - } - - Push to the remote: Bash(git push origin HEAD)`; - } else { - const branchName = eventData.claudeBranch || eventData.baseBranch; - return ` - - You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch. - - Use git commands via the Bash tool to commit and push your changes: - - Stage files: Bash(git add ) - - Commit with a descriptive message: Bash(git commit -m "") - ${ - coAuthorLine - ? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer: - Bash(git commit -m "\\n\\n${coAuthorLine}")` - : "" - } - - Push to the remote: Bash(git push origin ${branchName})`; - } - } -} - function substitutePromptVariables( template: string, context: PreparedContext, @@ -517,7 +501,7 @@ function substitutePromptVariables( export function generatePrompt( context: PreparedContext, githubData: FetchDataResult, - useCommitSigning: boolean, + useCommitSigning = false, ): string { if (context.overridePrompt) { return substitutePromptVariables( @@ -527,6 +511,8 @@ export function generatePrompt( ); } + const triggerDisplayName = context.triggerUsername ?? "Unknown"; + const { contextData, comments, @@ -594,7 +580,7 @@ ${ } ${context.claudeCommentId} ${context.triggerUsername ?? "Unknown"} -${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"} +${triggerDisplayName} ${context.triggerPhrase} ${ (eventData.eventName === "issue_comment" || diff --git a/src/create-prompt/types.ts b/src/create-prompt/types.ts index a69fdca..065a114 100644 --- a/src/create-prompt/types.ts +++ b/src/create-prompt/types.ts @@ -66,7 +66,7 @@ type IssueAssignedEvent = { issueNumber: string; baseBranch: string; claudeBranch?: string; - assigneeTrigger: string; + assigneeTrigger?: string; }; type IssueLabeledEvent = { diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 2f3ec12..914dffd 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -18,6 +18,7 @@ import { createPrompt } from "../create-prompt"; import { createClient } from "../github/api/client"; import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; +import { getMode } from "../modes/registry"; async function run() { try { @@ -54,9 +55,14 @@ async function run() { // Step 5: Check if actor is human await checkHumanActor(client.api, context); - // Step 6: Create initial tracking comment - const commentId = await createInitialComment(client.api, context); - core.setOutput("claude_comment_id", commentId.toString()); + const mode = getMode(context.inputs.mode); + + // Step 6: Create initial tracking comment (if required by mode) + let commentId: number | undefined; + if (mode.shouldCreateTrackingComment()) { + commentId = await createInitialComment(client.api, context); + core.setOutput("claude_comment_id", commentId!.toString()); + } // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) const githubData = await fetchGitHubData({ @@ -74,7 +80,7 @@ async function run() { } // Step 9: Update initial comment with branch link (only if a claude branch was created) - if (branchInfo.claudeBranch) { + if (commentId && branchInfo.claudeBranch) { await updateTrackingComment( client, context, @@ -84,21 +90,24 @@ async function run() { } // Step 10: Create prompt file - await createPrompt( + const modeContext = mode.prepareContext(context, { commentId, - branchInfo.baseBranch, - branchInfo.claudeBranch, - githubData, - context, - ); + baseBranch: branchInfo.baseBranch, + claudeBranch: branchInfo.claudeBranch, + }); + + await createPrompt(mode, modeContext, githubData, context); // Step 11: Get MCP configuration - const mcpConfig = await prepareMcpConfig( + const mcpConfig = await prepareMcpConfig({ githubToken, - context.repository.owner, - context.repository.repo, - branchInfo.currentBranch, - ); + owner: context.repository.owner, + repo: context.repository.repo, + branch: branchInfo.currentBranch, + baseBranch: branchInfo.baseBranch, + allowedTools: context.inputs.allowedTools, + context, + }); core.setOutput("mcp_config", mcpConfig); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/github/api/config.ts b/src/github/api/config.ts index 5776532..1cc5b90 100644 --- a/src/github/api/config.ts +++ b/src/github/api/config.ts @@ -29,3 +29,7 @@ export const GITEA_SERVER_URL = getServerUrl(); export const GITEA_API_URL = process.env.GITEA_API_URL || deriveApiUrl(GITEA_SERVER_URL); + +// Backwards-compatible aliases for legacy GitHub-specific naming +export const GITHUB_SERVER_URL = GITEA_SERVER_URL; +export const GITHUB_API_URL = GITEA_API_URL; diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 440d5a5..7a927fb 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -141,14 +141,16 @@ export function updateCommentBody(input: CommentUpdateInput): string { if (branchLink) { // Extract the branch URL from the link - const urlMatch = branchLink.match(/\((https:\/\/.*)\)/); + const urlMatch = branchLink.match(/\((https?:\/\/[^\)]+)\)/); if (urlMatch && urlMatch[1]) { branchUrl = urlMatch[1]; } // Extract branch name from link if not provided if (!finalBranchName) { - const branchNameMatch = branchLink.match(/tree\/([^"'\)]+)/); + const branchNameMatch = branchLink.match( + /(?:tree|src\/branch)\/([^"'\)\s]+)/, + ); if (branchNameMatch) { finalBranchName = branchNameMatch[1]; } @@ -157,10 +159,17 @@ export function updateCommentBody(input: CommentUpdateInput): string { // If we don't have a URL yet but have a branch name, construct it if (!branchUrl && finalBranchName) { - // Extract owner/repo from jobUrl - const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//); - if (repoMatch) { - branchUrl = `${GITEA_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/src/branch/${finalBranchName}`; + try { + const parsedJobUrl = new URL(jobUrl); + const segments = parsedJobUrl.pathname + .split("/") + .filter((segment) => segment); + const [owner, repo] = segments; + if (owner && repo) { + branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${finalBranchName}`; + } + } catch (error) { + console.warn(`Failed to derive branch URL from job URL: ${error}`); } } diff --git a/src/github/operations/comments/common.ts b/src/github/operations/comments/common.ts index 7c3793f..17100f8 100644 --- a/src/github/operations/comments/common.ts +++ b/src/github/operations/comments/common.ts @@ -1,6 +1,4 @@ import { GITEA_SERVER_URL } from "../../api/config"; -import { readFileSync } from "fs"; -import { join } from "path"; function getSpinnerHtml(): string { return ``; diff --git a/src/github/operations/git-config.ts b/src/github/operations/git-config.ts index 51a1c99..3b7aff7 100644 --- a/src/github/operations/git-config.ts +++ b/src/github/operations/git-config.ts @@ -7,7 +7,7 @@ import { $ } from "bun"; import type { ParsedGitHubContext } from "../context"; -import { GITHUB_SERVER_URL } from "../api/config"; +import { GITEA_SERVER_URL } from "../api/config"; type GitUser = { login: string; @@ -22,7 +22,7 @@ export async function configureGitAuth( console.log("Configuring git authentication for non-signing mode"); // Determine the noreply email domain based on GITHUB_SERVER_URL - const serverUrl = new URL(GITHUB_SERVER_URL); + const serverUrl = new URL(GITEA_SERVER_URL); const noreplyDomain = serverUrl.hostname === "github.com" ? "users.noreply.github.com" @@ -46,7 +46,7 @@ export async function configureGitAuth( // Remove the authorization header that actions/checkout sets console.log("Removing existing git authentication headers..."); try { - await $`git config --unset-all http.${GITHUB_SERVER_URL}/.extraheader`; + await $`git config --unset-all http.${GITEA_SERVER_URL}/.extraheader`; console.log("✓ Removed existing authentication headers"); } catch (e) { console.log("No existing authentication headers to remove"); diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 56fe6ab..41eae4e 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -8,6 +8,7 @@ import { isPullRequestReviewEvent, isPullRequestReviewCommentEvent, } from "../context"; +import type { IssuesLabeledEvent } from "@octokit/webhooks-types"; import type { ParsedGitHubContext } from "../context"; export function checkContainsTrigger(context: ParsedGitHubContext): boolean { @@ -41,6 +42,26 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { } } + // Check for issue label trigger + if (isIssuesEvent(context) && context.eventAction === "labeled") { + const triggerLabel = context.inputs.labelTrigger?.trim(); + const appliedLabel = (context.payload as IssuesLabeledEvent).label?.name + ?.trim(); + + console.log( + `Checking label trigger: expected='${triggerLabel}', applied='${appliedLabel}'`, + ); + + if ( + triggerLabel && + appliedLabel && + triggerLabel.localeCompare(appliedLabel, undefined, { sensitivity: "accent" }) === 0 + ) { + console.log(`Issue labeled with trigger label '${triggerLabel}'`); + return true; + } + } + // Check for issue body and title trigger on issue creation if (isIssuesEvent(context) && context.eventAction === "opened") { const issueBody = context.payload.issue.body || ""; diff --git a/src/mcp/gitea-mcp-server.ts b/src/mcp/gitea-mcp-server.ts index 5a60f4a..39ca0d1 100644 --- a/src/mcp/gitea-mcp-server.ts +++ b/src/mcp/gitea-mcp-server.ts @@ -942,7 +942,7 @@ server.tool( endpoint += `?style=${style}`; } - const result = await giteaRequest(endpoint, "POST"); + await giteaRequest(endpoint, "POST"); return { content: [ diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index ca1854f..a23e8f9 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -1,11 +1,24 @@ import * as core from "@actions/core"; +import type { ParsedGitHubContext } from "../github/context"; -export async function prepareMcpConfig( - githubToken: string, - owner: string, - repo: string, - branch: string, -): Promise { +export type PrepareMcpConfigOptions = { + githubToken: string; + owner: string; + repo: string; + branch: string; + baseBranch?: string; + allowedTools?: string[]; + context?: ParsedGitHubContext; + overrideConfig?: string; + additionalMcpConfig?: string; +}; + +export async function prepareMcpConfig({ + githubToken, + owner, + repo, + branch, +}: PrepareMcpConfigOptions): Promise { console.log("[MCP-INSTALL] Preparing MCP configuration..."); console.log(`[MCP-INSTALL] Owner: ${owner}`); console.log(`[MCP-INSTALL] Repo: ${repo}`); diff --git a/src/mcp/local-git-ops-server.ts b/src/mcp/local-git-ops-server.ts index 4373379..2971e3d 100644 --- a/src/mcp/local-git-ops-server.ts +++ b/src/mcp/local-git-ops-server.ts @@ -3,8 +3,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; -import { readFile, writeFile } from "fs/promises"; -import { join } from "path"; import { execSync } from "child_process"; // Get repository information from environment variables