feat: add mode-aware gitea prepare flow

This commit is contained in:
Mark Wylde
2025-09-30 17:31:36 +01:00
parent 582a02ee24
commit 3305a16297
13 changed files with 404 additions and 120 deletions

249
package-lock.json generated
View File

@@ -1,17 +1,18 @@
{ {
"name": "claude-pr-action", "name": "@anthropic-ai/claude-code-action",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "claude-pr-action", "name": "@anthropic-ai/claude-code-action",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@anthropic-ai/sdk": "^0.30.0", "@anthropic-ai/sdk": "^0.30.0",
"@modelcontextprotocol/sdk": "^1.11.0", "@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"zod": "^3.24.4" "zod": "^3.24.4"
@@ -209,6 +210,82 @@
"node": ">= 18" "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": { "node_modules/@octokit/openapi-types": {
"version": "24.2.0", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
@@ -304,6 +381,158 @@
"node": ">= 18" "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": { "node_modules/@octokit/types": {
"version": "13.10.0", "version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", "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" "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": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",

View File

@@ -14,6 +14,7 @@
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@anthropic-ai/sdk": "^0.30.0", "@anthropic-ai/sdk": "^0.30.0",
"@modelcontextprotocol/sdk": "^1.11.0", "@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"zod": "^3.24.4" "zod": "^3.24.4"

View File

@@ -19,7 +19,7 @@ import {
} from "../github/context"; } from "../github/context";
import type { ParsedGitHubContext } from "../github/context"; import type { ParsedGitHubContext } from "../github/context";
import type { CommonFields, PreparedContext, EventData } from "./types"; 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"; export type { CommonFields, PreparedContext } from "./types";
const BASE_ALLOWED_TOOLS = [ const BASE_ALLOWED_TOOLS = [
@@ -62,37 +62,74 @@ const BASE_ALLOWED_TOOLS = [
]; ];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"]; const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
export function buildAllowedToolsString( const ACTIONS_ALLOWED_TOOLS = [
customAllowedTools?: string[], "mcp__github_actions__get_ci_status",
): string { "mcp__github_actions__get_workflow_run_details",
let baseTools = [...BASE_ALLOWED_TOOLS]; "mcp__github_actions__download_job_log",
];
let allAllowedTools = baseTools.join(","); const COMMIT_SIGNING_TOOLS = [
if (customAllowedTools && customAllowedTools.length > 0) { "mcp__github_file_ops__commit_files",
allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`; "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<string>(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( export function buildDisallowedToolsString(
customDisallowedTools?: string[], customDisallowedTools?: string | string[],
allowedTools?: string[], allowedTools?: string | string[],
): string { ): string {
let disallowedTools = [...DISALLOWED_TOOLS]; let disallowedTools = [...DISALLOWED_TOOLS];
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list // If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
if (allowedTools && allowedTools.length > 0) { const allowedList = normalizeToolList(allowedTools);
disallowedTools = disallowedTools.filter( if (allowedList.length > 0) {
(tool) => !allowedTools.includes(tool), disallowedTools = disallowedTools.filter((tool) => !allowedList.includes(tool));
);
} }
let allDisallowedTools = disallowedTools.join(","); let allDisallowedTools = disallowedTools.join(",");
if (customDisallowedTools && customDisallowedTools.length > 0) { const customList = normalizeToolList(customDisallowedTools);
if (customList.length > 0) {
if (allDisallowedTools) { if (allDisallowedTools) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`; allDisallowedTools = `${allDisallowedTools},${customList.join(",")}`;
} else { } else {
allDisallowedTools = customDisallowedTools.join(","); allDisallowedTools = customList.join(",");
} }
} }
return allDisallowedTools; return allDisallowedTools;
@@ -240,6 +277,8 @@ export function prepareContext(
throw new Error( throw new Error(
"ISSUE_NUMBER is required for issue_comment event for issues", "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 = { eventData = {
@@ -249,7 +288,7 @@ export function prepareContext(
baseBranch, baseBranch,
issueNumber, issueNumber,
commentBody, commentBody,
...(claudeBranch && { claudeBranch }), claudeBranch,
}; };
break; break;
@@ -266,6 +305,9 @@ export function prepareContext(
if (!baseBranch) { if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issues event"); 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 (eventAction === "assigned") {
if (!assigneeTrigger && !directPrompt) { if (!assigneeTrigger && !directPrompt) {
@@ -279,8 +321,8 @@ export function prepareContext(
isPR: false, isPR: false,
issueNumber, issueNumber,
baseBranch, baseBranch,
assigneeTrigger, ...(assigneeTrigger && { assigneeTrigger }),
...(claudeBranch && { claudeBranch }), claudeBranch,
}; };
} else if (eventAction === "labeled") { } else if (eventAction === "labeled") {
if (!labelTrigger) { if (!labelTrigger) {
@@ -302,7 +344,7 @@ export function prepareContext(
isPR: false, isPR: false,
issueNumber, issueNumber,
baseBranch, baseBranch,
...(claudeBranch && { claudeBranch }), claudeBranch,
}; };
} else { } else {
throw new Error(`Unsupported issue action: ${eventAction}`); 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 <files>)
- Commit with a descriptive message: Bash(git commit -m "<message>")
${
coAuthorLine
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
Bash(git commit -m "<message>\\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 <files>)
- Commit with a descriptive message: Bash(git commit -m "<message>")
${
coAuthorLine
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
: ""
}
- Push to the remote: Bash(git push origin ${branchName})`;
}
}
}
function substitutePromptVariables( function substitutePromptVariables(
template: string, template: string,
context: PreparedContext, context: PreparedContext,
@@ -517,7 +501,7 @@ function substitutePromptVariables(
export function generatePrompt( export function generatePrompt(
context: PreparedContext, context: PreparedContext,
githubData: FetchDataResult, githubData: FetchDataResult,
useCommitSigning: boolean, useCommitSigning = false,
): string { ): string {
if (context.overridePrompt) { if (context.overridePrompt) {
return substitutePromptVariables( return substitutePromptVariables(
@@ -527,6 +511,8 @@ export function generatePrompt(
); );
} }
const triggerDisplayName = context.triggerUsername ?? "Unknown";
const { const {
contextData, contextData,
comments, comments,
@@ -594,7 +580,7 @@ ${
} }
<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>${triggerDisplayName}</trigger_display_name>
<trigger_phrase>${context.triggerPhrase}</trigger_phrase> <trigger_phrase>${context.triggerPhrase}</trigger_phrase>
${ ${
(eventData.eventName === "issue_comment" || (eventData.eventName === "issue_comment" ||

View File

@@ -66,7 +66,7 @@ type IssueAssignedEvent = {
issueNumber: string; issueNumber: string;
baseBranch: string; baseBranch: string;
claudeBranch?: string; claudeBranch?: string;
assigneeTrigger: string; assigneeTrigger?: string;
}; };
type IssueLabeledEvent = { type IssueLabeledEvent = {

View File

@@ -18,6 +18,7 @@ import { createPrompt } from "../create-prompt";
import { createClient } from "../github/api/client"; import { createClient } from "../github/api/client";
import { fetchGitHubData } from "../github/data/fetcher"; import { fetchGitHubData } from "../github/data/fetcher";
import { parseGitHubContext } from "../github/context"; import { parseGitHubContext } from "../github/context";
import { getMode } from "../modes/registry";
async function run() { async function run() {
try { try {
@@ -54,9 +55,14 @@ async function run() {
// Step 5: Check if actor is human // Step 5: Check if actor is human
await checkHumanActor(client.api, context); await checkHumanActor(client.api, context);
// Step 6: Create initial tracking comment const mode = getMode(context.inputs.mode);
const commentId = await createInitialComment(client.api, context);
core.setOutput("claude_comment_id", commentId.toString()); // 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) // Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
const githubData = await fetchGitHubData({ 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) // Step 9: Update initial comment with branch link (only if a claude branch was created)
if (branchInfo.claudeBranch) { if (commentId && branchInfo.claudeBranch) {
await updateTrackingComment( await updateTrackingComment(
client, client,
context, context,
@@ -84,21 +90,24 @@ async function run() {
} }
// Step 10: Create prompt file // Step 10: Create prompt file
await createPrompt( const modeContext = mode.prepareContext(context, {
commentId, commentId,
branchInfo.baseBranch, baseBranch: branchInfo.baseBranch,
branchInfo.claudeBranch, claudeBranch: branchInfo.claudeBranch,
githubData, });
context,
); await createPrompt(mode, modeContext, githubData, context);
// Step 11: Get MCP configuration // Step 11: Get MCP configuration
const mcpConfig = await prepareMcpConfig( const mcpConfig = await prepareMcpConfig({
githubToken, githubToken,
context.repository.owner, owner: context.repository.owner,
context.repository.repo, repo: context.repository.repo,
branchInfo.currentBranch, branch: branchInfo.currentBranch,
); baseBranch: branchInfo.baseBranch,
allowedTools: context.inputs.allowedTools,
context,
});
core.setOutput("mcp_config", mcpConfig); core.setOutput("mcp_config", mcpConfig);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);

View File

@@ -29,3 +29,7 @@ export const GITEA_SERVER_URL = getServerUrl();
export const GITEA_API_URL = export const GITEA_API_URL =
process.env.GITEA_API_URL || deriveApiUrl(GITEA_SERVER_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;

View File

@@ -141,14 +141,16 @@ export function updateCommentBody(input: CommentUpdateInput): string {
if (branchLink) { if (branchLink) {
// Extract the branch URL from the link // Extract the branch URL from the link
const urlMatch = branchLink.match(/\((https:\/\/.*)\)/); const urlMatch = branchLink.match(/\((https?:\/\/[^\)]+)\)/);
if (urlMatch && urlMatch[1]) { if (urlMatch && urlMatch[1]) {
branchUrl = urlMatch[1]; branchUrl = urlMatch[1];
} }
// Extract branch name from link if not provided // Extract branch name from link if not provided
if (!finalBranchName) { if (!finalBranchName) {
const branchNameMatch = branchLink.match(/tree\/([^"'\)]+)/); const branchNameMatch = branchLink.match(
/(?:tree|src\/branch)\/([^"'\)\s]+)/,
);
if (branchNameMatch) { if (branchNameMatch) {
finalBranchName = branchNameMatch[1]; 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 we don't have a URL yet but have a branch name, construct it
if (!branchUrl && finalBranchName) { if (!branchUrl && finalBranchName) {
// Extract owner/repo from jobUrl try {
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//); const parsedJobUrl = new URL(jobUrl);
if (repoMatch) { const segments = parsedJobUrl.pathname
branchUrl = `${GITEA_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/src/branch/${finalBranchName}`; .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}`);
} }
} }

View File

@@ -1,6 +1,4 @@
import { GITEA_SERVER_URL } from "../../api/config"; import { GITEA_SERVER_URL } from "../../api/config";
import { readFileSync } from "fs";
import { join } from "path";
function getSpinnerHtml(): string { function getSpinnerHtml(): string {
return `<img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`; return `<img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`;

View File

@@ -7,7 +7,7 @@
import { $ } from "bun"; import { $ } from "bun";
import type { ParsedGitHubContext } from "../context"; import type { ParsedGitHubContext } from "../context";
import { GITHUB_SERVER_URL } from "../api/config"; import { GITEA_SERVER_URL } from "../api/config";
type GitUser = { type GitUser = {
login: string; login: string;
@@ -22,7 +22,7 @@ export async function configureGitAuth(
console.log("Configuring git authentication for non-signing mode"); console.log("Configuring git authentication for non-signing mode");
// Determine the noreply email domain based on GITHUB_SERVER_URL // 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 = const noreplyDomain =
serverUrl.hostname === "github.com" serverUrl.hostname === "github.com"
? "users.noreply.github.com" ? "users.noreply.github.com"
@@ -46,7 +46,7 @@ export async function configureGitAuth(
// Remove the authorization header that actions/checkout sets // Remove the authorization header that actions/checkout sets
console.log("Removing existing git authentication headers..."); console.log("Removing existing git authentication headers...");
try { 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"); console.log("✓ Removed existing authentication headers");
} catch (e) { } catch (e) {
console.log("No existing authentication headers to remove"); console.log("No existing authentication headers to remove");

View File

@@ -8,6 +8,7 @@ import {
isPullRequestReviewEvent, isPullRequestReviewEvent,
isPullRequestReviewCommentEvent, isPullRequestReviewCommentEvent,
} from "../context"; } from "../context";
import type { IssuesLabeledEvent } from "@octokit/webhooks-types";
import type { ParsedGitHubContext } from "../context"; import type { ParsedGitHubContext } from "../context";
export function checkContainsTrigger(context: ParsedGitHubContext): boolean { 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 // Check for issue body and title trigger on issue creation
if (isIssuesEvent(context) && context.eventAction === "opened") { if (isIssuesEvent(context) && context.eventAction === "opened") {
const issueBody = context.payload.issue.body || ""; const issueBody = context.payload.issue.body || "";

View File

@@ -942,7 +942,7 @@ server.tool(
endpoint += `?style=${style}`; endpoint += `?style=${style}`;
} }
const result = await giteaRequest(endpoint, "POST"); await giteaRequest(endpoint, "POST");
return { return {
content: [ content: [

View File

@@ -1,11 +1,24 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import type { ParsedGitHubContext } from "../github/context";
export async function prepareMcpConfig( export type PrepareMcpConfigOptions = {
githubToken: string, githubToken: string;
owner: string, owner: string;
repo: string, repo: string;
branch: string, branch: string;
): Promise<string> { baseBranch?: string;
allowedTools?: string[];
context?: ParsedGitHubContext;
overrideConfig?: string;
additionalMcpConfig?: string;
};
export async function prepareMcpConfig({
githubToken,
owner,
repo,
branch,
}: PrepareMcpConfigOptions): Promise<string> {
console.log("[MCP-INSTALL] Preparing MCP configuration..."); console.log("[MCP-INSTALL] Preparing MCP configuration...");
console.log(`[MCP-INSTALL] Owner: ${owner}`); console.log(`[MCP-INSTALL] Owner: ${owner}`);
console.log(`[MCP-INSTALL] Repo: ${repo}`); console.log(`[MCP-INSTALL] Repo: ${repo}`);

View File

@@ -3,8 +3,6 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { execSync } from "child_process"; import { execSync } from "child_process";
// Get repository information from environment variables // Get repository information from environment variables