From f2f966c77eef99d1694cf0e08932d83ee274192c Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Fri, 30 May 2025 21:00:03 +0100 Subject: [PATCH] Attempt to make this work --- MIGRATION.md | 32 ++++- action.yml | 3 +- examples/gitea-claude.yml | 6 +- src/claude/executor.ts | 28 ++-- src/entrypoints/execute-claude.ts | 10 +- src/entrypoints/update-comment-link.ts | 63 ++++++++- src/github/api/client.ts | 2 +- src/github/data/fetcher.ts | 18 ++- src/github/operations/branch-cleanup.ts | 59 ++++++++- src/github/operations/branch.ts | 26 ++-- src/github/token.ts | 9 +- src/github/utils/image-downloader.ts | 162 +++++++++++++++++------- src/github/validation/actor.ts | 21 ++- src/github/validation/permissions.ts | 6 +- src/github/validation/trigger.ts | 8 +- 15 files changed, 349 insertions(+), 104 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 641e5a1..714a86e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -5,16 +5,19 @@ This document outlines the changes made to migrate from GitHub App authenticatio ## What Changed ### 1. Removed GitHub App Dependencies + - **Before**: Used OIDC token exchange with Anthropic's GitHub App service - **After**: Uses standard `GITHUB_TOKEN` from workflow environment - **Benefit**: No external dependencies, works with any Git provider ### 2. Self-Contained Implementation + - **Before**: Depended on external `anthropics/claude-code-base-action` - **After**: Includes built-in Claude execution engine - **Benefit**: Complete control over functionality, no external action dependencies ### 3. Gitea Compatibility + - **Before**: GitHub-specific triggers and authentication - **After**: Compatible with Gitea Actions (with some limitations) - **Benefit**: Works with self-hosted Gitea instances @@ -22,6 +25,7 @@ This document outlines the changes made to migrate from GitHub App authenticatio ## Required Changes for Existing Users ### Workflow Permissions + Update your workflow permissions: ```yaml @@ -40,6 +44,7 @@ permissions: ``` ### Required Token Input + Now required to explicitly provide a GitHub token: ```yaml @@ -58,6 +63,7 @@ Now required to explicitly provide a GitHub token: ## Gitea Setup ### 1. Basic Gitea Workflow + Use the example in `examples/gitea-claude.yml`: ```yaml @@ -86,13 +92,14 @@ jobs: fetch-depth: 0 - name: Run Claude Assistant - uses: ./ # Adjust path as needed for your Gitea setup + uses: ./ # Adjust path as needed for your Gitea setup with: github_token: ${{ secrets.GITHUB_TOKEN }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} ``` ### 2. Gitea Limitations + Be aware of these Gitea Actions limitations: - **`issue_comment` on PRs**: May not trigger reliably in some Gitea versions @@ -105,16 +112,19 @@ Be aware of these Gitea Actions limitations: ### 3. Gitea Workarounds #### For PR Comments + Use `issue_comment` instead of `pull_request_review_comment`: ```yaml on: issue_comment: - types: [created] # This covers both issue and PR comments + types: [created] # This covers both issue and PR comments ``` #### For Code Review Comments + Gitea has limited support for code review comment webhooks. Consider using: + - Regular issue comments on PRs - Manual trigger via issue assignment - Custom webhooks (advanced setup) @@ -122,21 +132,25 @@ Gitea has limited support for code review comment webhooks. Consider using: ## Benefits of Migration ### 1. Simplified Authentication + - No OIDC token setup required - Uses standard workflow tokens - Works with custom GitHub tokens ### 2. Provider Independence + - No dependency on Anthropic's GitHub App service - Works with any Git provider supporting Actions - Self-contained functionality ### 3. Enhanced Control + - Direct control over Claude execution - Customizable tool management - Easier debugging and modifications ### 4. Gitea Support + - Compatible with self-hosted Gitea - Automatic fallback to REST API (no GraphQL dependency) - Simplified permission checking for Gitea environments @@ -148,8 +162,10 @@ Gitea has limited support for code review comment webhooks. Consider using: ### Common Issues #### 1. Token Permissions + **Error**: "GitHub token authentication failed" **Solution**: Ensure workflow has required permissions: + ```yaml permissions: contents: write @@ -158,36 +174,45 @@ permissions: ``` #### 2. Gitea Trigger Issues + **Error**: Workflow not triggering on PR comments **Solution**: Use `issue_comment` instead of `pull_request_review_comment` #### 3. Missing Dependencies + **Error**: "Module not found" or TypeScript errors **Solution**: Run `npm install` or `bun install` to update dependencies ### Gitea-Specific Issues #### 1. Authentication Errors + **Error**: "Failed to check permissions: HttpError: Bad credentials" **Solution**: This is normal in Gitea environments. The action automatically detects Gitea and bypasses GitHub-specific permission checks. #### 1a. User Profile API Errors + **Error**: "Prepare step failed with error: Visit Project" or "GET /users/{username} - 404" **Solution**: This occurs when Gitea's user profile API differs from GitHub's. The action automatically detects Gitea and skips user type validation. #### 2. Limited Event Support + Some GitHub Events may not be fully supported in Gitea. Use basic triggers: + - `issue_comment` for comments - `issues` for issue events - `push` for code changes #### 3. Token Scope Limitations + Gitea tokens may have different scope limitations. Ensure your Gitea instance allows: + - Repository write access - Issue/PR comment creation - Branch creation and updates #### 4. GraphQL Not Supported + **Error**: GraphQL queries failing **Solution**: The action automatically detects Gitea and uses REST API instead of GraphQL. No manual configuration needed. @@ -204,5 +229,6 @@ Gitea tokens may have different scope limitations. Ensure your Gitea instance al ## Example Workflows See the `examples/` directory for complete workflow examples: + - `claude.yml` - Updated GitHub Actions workflow -- `gitea-claude.yml` - Gitea-compatible workflow \ No newline at end of file +- `gitea-claude.yml` - Gitea-compatible workflow diff --git a/action.yml b/action.yml index f28a9ac..dde0eeb 100644 --- a/action.yml +++ b/action.yml @@ -114,7 +114,7 @@ runs: USE_BEDROCK: ${{ inputs.use_bedrock }} USE_VERTEX: ${{ inputs.use_vertex }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} - + # GitHub token for repository access GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} @@ -163,4 +163,3 @@ runs: echo '```json' >> $GITHUB_STEP_SUMMARY cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - diff --git a/examples/gitea-claude.yml b/examples/gitea-claude.yml index d3a8b2f..e6acf7b 100644 --- a/examples/gitea-claude.yml +++ b/examples/gitea-claude.yml @@ -29,9 +29,9 @@ jobs: fetch-depth: 0 - name: Run Claude Assistant - uses: ./ # Use local action (adjust path as needed) + uses: ./ # Use local action (adjust path as needed) with: - github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token + github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} timeout_minutes: "60" trigger_phrase: "@claude" @@ -40,4 +40,4 @@ jobs: You are working in a Gitea environment. Be aware that: - Some GitHub Actions features may behave differently - Focus on core functionality and avoid advanced GitHub-specific features - - Use standard git operations when possible \ No newline at end of file + - Use standard git operations when possible diff --git a/src/claude/executor.ts b/src/claude/executor.ts index 32c15b7..0c8f193 100644 --- a/src/claude/executor.ts +++ b/src/claude/executor.ts @@ -34,7 +34,9 @@ export class ClaudeExecutor { private initializeClient() { if (this.config.useBedrock || this.config.useVertex) { - throw new Error("Bedrock and Vertex AI not supported in simplified implementation"); + throw new Error( + "Bedrock and Vertex AI not supported in simplified implementation", + ); } if (!this.config.apiKey) { @@ -63,11 +65,17 @@ export class ClaudeExecutor { private parseTools(): { allowed: string[]; disallowed: string[] } { const allowed = this.config.allowedTools - ? this.config.allowedTools.split(",").map(t => t.trim()).filter(Boolean) + ? this.config.allowedTools + .split(",") + .map((t) => t.trim()) + .filter(Boolean) : []; - + const disallowed = this.config.disallowedTools - ? this.config.disallowedTools.split(",").map(t => t.trim()).filter(Boolean) + ? this.config.disallowedTools + .split(",") + .map((t) => t.trim()) + .filter(Boolean) : []; return { allowed, disallowed }; @@ -92,7 +100,9 @@ export class ClaudeExecutor { const prompt = await this.readPrompt(); const tools = this.parseTools(); - console.log(`Executing Claude with model: ${this.config.model || "claude-3-7-sonnet-20250219"}`); + console.log( + `Executing Claude with model: ${this.config.model || "claude-3-7-sonnet-20250219"}`, + ); console.log(`Allowed tools: ${tools.allowed.join(", ") || "none"}`); console.log(`Disallowed tools: ${tools.disallowed.join(", ") || "none"}`); @@ -122,7 +132,7 @@ export class ClaudeExecutor { }; } catch (error) { console.error("Claude execution failed:", error); - + const executionFile = this.createExecutionLog(null, String(error)); return { @@ -134,7 +144,9 @@ export class ClaudeExecutor { } } -export async function runClaude(config: ClaudeExecutorConfig): Promise { +export async function runClaude( + config: ClaudeExecutorConfig, +): Promise { const executor = new ClaudeExecutor(config); return await executor.execute(); -} \ No newline at end of file +} diff --git a/src/entrypoints/execute-claude.ts b/src/entrypoints/execute-claude.ts index faaadca..e85c605 100644 --- a/src/entrypoints/execute-claude.ts +++ b/src/entrypoints/execute-claude.ts @@ -10,8 +10,12 @@ async function main() { model: process.env.ANTHROPIC_MODEL || process.env.MODEL, promptFile: process.env.PROMPT_FILE, prompt: process.env.PROMPT, - maxTurns: process.env.MAX_TURNS ? parseInt(process.env.MAX_TURNS) : undefined, - timeoutMinutes: process.env.TIMEOUT_MINUTES ? parseInt(process.env.TIMEOUT_MINUTES) : 30, + maxTurns: process.env.MAX_TURNS + ? parseInt(process.env.MAX_TURNS) + : undefined, + timeoutMinutes: process.env.TIMEOUT_MINUTES + ? parseInt(process.env.TIMEOUT_MINUTES) + : 30, mcpConfig: process.env.MCP_CONFIG, allowedTools: process.env.ALLOWED_TOOLS, disallowedTools: process.env.DISALLOWED_TOOLS, @@ -43,4 +47,4 @@ main().catch((error) => { console.error("Unhandled error:", error); core.setFailed(`Unhandled error: ${error}`); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 40c20ad..fb73d45 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -133,7 +133,68 @@ async function run() { } } catch (error) { console.error("Error checking for changes in branch:", error); - // Don't fail the entire update if we can't check for changes + + // For Gitea compatibility, try alternative approach + try { + console.log( + "Trying alternative branch comparison for Gitea compatibility...", + ); + + // Get the branch info to see if it exists and has commits + const branchResponse = await octokit.rest.repos.getBranch({ + owner, + repo, + branch: claudeBranch, + }); + + // Get base branch info for comparison + const baseResponse = await octokit.rest.repos.getBranch({ + owner, + repo, + branch: baseBranch, + }); + + const branchSha = branchResponse.data.commit.sha; + const baseSha = baseResponse.data.commit.sha; + + // If SHAs are different, assume there are changes and add PR link + if (branchSha !== baseSha) { + console.log( + `Branch ${claudeBranch} appears to have changes (different SHA from base)`, + ); + const entityType = context.isPR ? "PR" : "Issue"; + const prTitle = encodeURIComponent( + `${entityType} #${context.entityNumber}: Changes from Claude`, + ); + const prBody = encodeURIComponent( + `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, + ); + const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; + prLink = `\n[Create a PR](${prUrl})`; + } else { + console.log( + `Branch ${claudeBranch} has same SHA as base, no PR link needed`, + ); + } + } catch (fallbackError) { + console.error( + "Fallback branch comparison also failed:", + fallbackError, + ); + // If all checks fail, still add PR link to be safe + console.log( + "Adding PR link as fallback since we can't determine branch status", + ); + const entityType = context.isPR ? "PR" : "Issue"; + const prTitle = encodeURIComponent( + `${entityType} #${context.entityNumber}: Changes from Claude`, + ); + const prBody = encodeURIComponent( + `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, + ); + const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; + prLink = `\n[Create a PR](${prUrl})`; + } } } } diff --git a/src/github/api/client.ts b/src/github/api/client.ts index 6bd4c71..5437058 100644 --- a/src/github/api/client.ts +++ b/src/github/api/client.ts @@ -9,7 +9,7 @@ export type Octokits = { export function createOctokit(token: string): Octokits { return { - rest: new Octokit({ + rest: new Octokit({ auth: token, baseUrl: GITHUB_API_URL, }), diff --git a/src/github/data/fetcher.ts b/src/github/data/fetcher.ts index a08470c..7696257 100644 --- a/src/github/data/fetcher.ts +++ b/src/github/data/fetcher.ts @@ -45,7 +45,9 @@ export async function fetchGitHubData({ } // Check if we're in a Gitea environment (no GraphQL support) - const isGitea = process.env.GITHUB_API_URL && !process.env.GITHUB_API_URL.includes('api.github.com'); + const isGitea = + process.env.GITHUB_API_URL && + !process.env.GITHUB_API_URL.includes("api.github.com"); let contextData: GitHubPullRequest | GitHubIssue | null = null; let comments: GitHubComment[] = []; @@ -56,7 +58,9 @@ export async function fetchGitHubData({ if (isGitea) { // Use REST API for Gitea compatibility if (isPR) { - console.log(`Fetching PR #${prNumber} data using REST API (Gitea mode)`); + console.log( + `Fetching PR #${prNumber} data using REST API (Gitea mode)`, + ); const prResponse = await octokits.rest.pulls.get({ owner, repo, @@ -87,7 +91,7 @@ export async function fetchGitHubData({ repo, issue_number: parseInt(prNumber), }); - comments = commentsResponse.data.map(comment => ({ + comments = commentsResponse.data.map((comment) => ({ id: comment.id.toString(), databaseId: comment.id.toString(), body: comment.body || "", @@ -106,7 +110,7 @@ export async function fetchGitHubData({ repo, pull_number: parseInt(prNumber), }); - changedFiles = filesResponse.data.map(file => ({ + changedFiles = filesResponse.data.map((file) => ({ path: file.filename, additions: file.additions || 0, deletions: file.deletions || 0, @@ -119,7 +123,9 @@ export async function fetchGitHubData({ reviewData = { nodes: [] }; // Simplified for Gitea } else { - console.log(`Fetching issue #${prNumber} data using REST API (Gitea mode)`); + console.log( + `Fetching issue #${prNumber} data using REST API (Gitea mode)`, + ); const issueResponse = await octokits.rest.issues.get({ owner, repo, @@ -142,7 +148,7 @@ export async function fetchGitHubData({ repo, issue_number: parseInt(prNumber), }); - comments = commentsResponse.data.map(comment => ({ + comments = commentsResponse.data.map((comment) => ({ id: comment.id.toString(), databaseId: comment.id.toString(), body: comment.body || "", diff --git a/src/github/operations/branch-cleanup.ts b/src/github/operations/branch-cleanup.ts index 662a474..374a9da 100644 --- a/src/github/operations/branch-cleanup.ts +++ b/src/github/operations/branch-cleanup.ts @@ -34,9 +34,49 @@ export async function checkAndDeleteEmptyBranch( } } catch (error) { console.error("Error checking for commits on Claude branch:", error); - // If we can't check, assume the branch has commits to be safe - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; - branchLink = `\n[View branch](${branchUrl})`; + + // For Gitea compatibility, try alternative approach using branches endpoint + try { + console.log( + "Trying alternative branch comparison for Gitea compatibility...", + ); + + // Get the branch info to see if it exists and has commits + const branchResponse = await octokit.rest.repos.getBranch({ + owner, + repo, + branch: claudeBranch, + }); + + // Get base branch info for comparison + const baseResponse = await octokit.rest.repos.getBranch({ + owner, + repo, + branch: baseBranch, + }); + + const branchSha = branchResponse.data.commit.sha; + const baseSha = baseResponse.data.commit.sha; + + // If SHAs are different, assume there are commits + if (branchSha !== baseSha) { + console.log( + `Branch ${claudeBranch} appears to have commits (different SHA from base)`, + ); + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + branchLink = `\n[View branch](${branchUrl})`; + } else { + console.log( + `Branch ${claudeBranch} has same SHA as base, marking for deletion`, + ); + shouldDeleteBranch = true; + } + } catch (fallbackError) { + console.error("Fallback branch comparison also failed:", fallbackError); + // If all checks fail, assume the branch has commits to be safe + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + branchLink = `\n[View branch](${branchUrl})`; + } } } @@ -49,9 +89,18 @@ export async function checkAndDeleteEmptyBranch( ref: `heads/${claudeBranch}`, }); console.log(`✅ Deleted empty branch: ${claudeBranch}`); - } catch (deleteError) { + } catch (deleteError: any) { console.error(`Failed to delete branch ${claudeBranch}:`, deleteError); - // Continue even if deletion fails + console.log(`Delete error status: ${deleteError.status}`); + + // For Gitea, branch deletion might not be supported via API + if (deleteError.status === 405 || deleteError.status === 404) { + console.log( + "Branch deletion not supported or branch doesn't exist remotely - this is expected for Gitea", + ); + } + + // Continue even if deletion fails - this is not critical } } diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index b8b52f8..b44270d 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -96,7 +96,7 @@ export async function setupBranch( // Get the SHA of the source branch // For Gitea, try using the branches endpoint instead of git/refs let currentSHA: string; - + try { // First try the GitHub-compatible git.getRef approach const sourceBranchRef = await octokits.rest.git.getRef({ @@ -107,14 +107,16 @@ export async function setupBranch( currentSHA = sourceBranchRef.data.object.sha; } catch (gitRefError: any) { // If git/refs fails (like in Gitea), use the branches endpoint - console.log(`git/refs failed, trying branches endpoint: ${gitRefError.message}`); + console.log( + `git/refs failed, trying branches endpoint: ${gitRefError.message}`, + ); const branchResponse = await octokits.rest.repos.getBranch({ owner, repo, branch: sourceBranch, }); - // Gitea uses commit.id instead of commit.sha - currentSHA = branchResponse.data.commit.sha || branchResponse.data.commit.id; + // GitHub and Gitea both use commit.sha + currentSHA = branchResponse.data.commit.sha; } console.log(`Current SHA: ${currentSHA}`); @@ -127,18 +129,22 @@ export async function setupBranch( ref: `refs/heads/${newBranch}`, sha: currentSHA, }); - + console.log(`Successfully created branch via API: ${newBranch}`); } catch (createRefError: any) { // If git/refs creation fails (like in Gitea), that's expected - // We'll create the branch when we push files later - console.log(`git createRef failed (expected for Gitea): ${createRefError.message}`); + // Log the error details but continue - the branch will be created when we push files + console.log( + `git createRef failed (expected for Gitea): ${createRefError.message}`, + ); + console.log(`Error status: ${createRefError.status}`); console.log(`Branch ${newBranch} will be created when files are pushed`); + + // For Gitea, we can still proceed since the MCP server will create the branch on first push + // This is actually the preferred method for Gitea } - console.log( - `Branch setup completed for: ${newBranch}`, - ); + console.log(`Branch setup completed for: ${newBranch}`); // Set outputs for GitHub Actions core.setOutput("CLAUDE_BRANCH", newBranch); diff --git a/src/github/token.ts b/src/github/token.ts index d879ab9..a6b1ab0 100644 --- a/src/github/token.ts +++ b/src/github/token.ts @@ -2,9 +2,6 @@ import * as core from "@actions/core"; - - - export async function setupGitHubToken(): Promise { try { // Check if GitHub token was provided as override @@ -18,14 +15,16 @@ export async function setupGitHubToken(): Promise { // Use the standard GITHUB_TOKEN from the workflow environment const workflowToken = process.env.GITHUB_TOKEN; - + if (workflowToken) { console.log("Using workflow GITHUB_TOKEN for authentication"); core.setOutput("GITHUB_TOKEN", workflowToken); return workflowToken; } - throw new Error("No GitHub token available. Please provide a github_token input or ensure GITHUB_TOKEN is available in the workflow environment."); + throw new Error( + "No GitHub token available. Please provide a github_token input or ensure GITHUB_TOKEN is available in the workflow environment.", + ); } catch (error) { core.setFailed( `Failed to setup GitHub token: ${error}.\n\nPlease provide a \`github_token\` in the \`with\` section of the action in your workflow yml file, or ensure the workflow has access to the default GITHUB_TOKEN.`, diff --git a/src/github/utils/image-downloader.ts b/src/github/utils/image-downloader.ts index 40cc974..f450fd1 100644 --- a/src/github/utils/image-downloader.ts +++ b/src/github/utils/image-downloader.ts @@ -84,67 +84,135 @@ export async function downloadCommentImages( let bodyHtml: string | undefined; // Get the HTML version based on comment type + // Try with full+json mediaType first (GitHub), fallback to regular API (Gitea) switch (comment.type) { case "issue_comment": { - const response = await octokits.rest.issues.getComment({ - owner, - repo, - comment_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; + try { + const response = await octokits.rest.issues.getComment({ + owner, + repo, + comment_id: parseInt(comment.id), + mediaType: { + format: "full+json", + }, + }); + bodyHtml = response.data.body_html; + } catch (error: any) { + console.log( + "Full+json format not supported, trying regular API for issue comment", + ); + // Fallback for Gitea - use regular API without mediaType + const response = await octokits.rest.issues.getComment({ + owner, + repo, + comment_id: parseInt(comment.id), + }); + // Gitea might not have body_html, use body instead + bodyHtml = (response.data as any).body_html || response.data.body; + } break; } case "review_comment": { - const response = await octokits.rest.pulls.getReviewComment({ - owner, - repo, - comment_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; + try { + const response = await octokits.rest.pulls.getReviewComment({ + owner, + repo, + comment_id: parseInt(comment.id), + mediaType: { + format: "full+json", + }, + }); + bodyHtml = response.data.body_html; + } catch (error: any) { + console.log( + "Full+json format not supported, trying regular API for review comment", + ); + // Fallback for Gitea + const response = await octokits.rest.pulls.getReviewComment({ + owner, + repo, + comment_id: parseInt(comment.id), + }); + bodyHtml = (response.data as any).body_html || response.data.body; + } break; } case "review_body": { - const response = await octokits.rest.pulls.getReview({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - review_id: parseInt(comment.id), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; + try { + const response = await octokits.rest.pulls.getReview({ + owner, + repo, + pull_number: parseInt(comment.pullNumber), + review_id: parseInt(comment.id), + mediaType: { + format: "full+json", + }, + }); + bodyHtml = response.data.body_html; + } catch (error: any) { + console.log( + "Full+json format not supported, trying regular API for review", + ); + // Fallback for Gitea + const response = await octokits.rest.pulls.getReview({ + owner, + repo, + pull_number: parseInt(comment.pullNumber), + review_id: parseInt(comment.id), + }); + bodyHtml = (response.data as any).body_html || response.data.body; + } break; } case "issue_body": { - const response = await octokits.rest.issues.get({ - owner, - repo, - issue_number: parseInt(comment.issueNumber), - mediaType: { - format: "full+json", - }, - }); - bodyHtml = response.data.body_html; + try { + const response = await octokits.rest.issues.get({ + owner, + repo, + issue_number: parseInt(comment.issueNumber), + mediaType: { + format: "full+json", + }, + }); + bodyHtml = response.data.body_html; + } catch (error: any) { + console.log( + "Full+json format not supported, trying regular API for issue", + ); + // Fallback for Gitea + const response = await octokits.rest.issues.get({ + owner, + repo, + issue_number: parseInt(comment.issueNumber), + }); + bodyHtml = (response.data as any).body_html || response.data.body; + } break; } case "pr_body": { - const response = await octokits.rest.pulls.get({ - owner, - repo, - pull_number: parseInt(comment.pullNumber), - mediaType: { - format: "full+json", - }, - }); - // Type here seems to be wrong - bodyHtml = (response.data as any).body_html; + try { + const response = await octokits.rest.pulls.get({ + owner, + repo, + pull_number: parseInt(comment.pullNumber), + mediaType: { + format: "full+json", + }, + }); + // Type here seems to be wrong + bodyHtml = (response.data as any).body_html; + } catch (error: any) { + console.log( + "Full+json format not supported, trying regular API for PR", + ); + // Fallback for Gitea + const response = await octokits.rest.pulls.get({ + owner, + repo, + pull_number: parseInt(comment.pullNumber), + }); + bodyHtml = (response.data as any).body_html || response.data.body; + } break; } } diff --git a/src/github/validation/actor.ts b/src/github/validation/actor.ts index 4d4c493..e8e8375 100644 --- a/src/github/validation/actor.ts +++ b/src/github/validation/actor.ts @@ -13,10 +13,14 @@ export async function checkHumanActor( githubContext: ParsedGitHubContext, ) { // Check if we're in a Gitea environment - const isGitea = process.env.GITHUB_API_URL && !process.env.GITHUB_API_URL.includes('api.github.com'); - + const isGitea = + process.env.GITHUB_API_URL && + !process.env.GITHUB_API_URL.includes("api.github.com"); + if (isGitea) { - console.log(`Detected Gitea environment, skipping actor type validation for: ${githubContext.actor}`); + console.log( + `Detected Gitea environment, skipping actor type validation for: ${githubContext.actor}`, + ); return; } @@ -38,9 +42,14 @@ export async function checkHumanActor( console.log(`Verified human actor: ${githubContext.actor}`); } catch (error) { - console.warn(`Failed to check actor type for ${githubContext.actor}:`, error); - + console.warn( + `Failed to check actor type for ${githubContext.actor}:`, + error, + ); + // For compatibility, assume human actor if API call fails - console.log(`Assuming human actor due to API failure: ${githubContext.actor}`); + console.log( + `Assuming human actor due to API failure: ${githubContext.actor}`, + ); } } diff --git a/src/github/validation/permissions.ts b/src/github/validation/permissions.ts index 5ee7916..ca9696b 100644 --- a/src/github/validation/permissions.ts +++ b/src/github/validation/permissions.ts @@ -15,8 +15,10 @@ export async function checkWritePermissions( const { repository, actor } = context; // For Gitea compatibility, check if we're in a non-GitHub environment - const isGitea = process.env.GITHUB_API_URL && !process.env.GITHUB_API_URL.includes('api.github.com'); - + const isGitea = + process.env.GITHUB_API_URL && + !process.env.GITHUB_API_URL.includes("api.github.com"); + if (isGitea) { core.info(`Detected Gitea environment, assuming actor has permissions`); return true; diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index ea477a1..56fe6ab 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -15,7 +15,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { inputs: { assigneeTrigger, triggerPhrase, directPrompt }, } = context; - console.log(`Checking trigger: event=${context.eventName}, action=${context.eventAction}, phrase='${triggerPhrase}', assignee='${assigneeTrigger}', direct='${directPrompt}'`); + console.log( + `Checking trigger: event=${context.eventName}, action=${context.eventAction}, phrase='${triggerPhrase}', assignee='${assigneeTrigger}', direct='${directPrompt}'`, + ); // If direct prompt is provided, always trigger if (directPrompt) { @@ -29,7 +31,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { let triggerUser = assigneeTrigger?.replace(/^@/, "") || ""; const assigneeUsername = context.payload.issue.assignee?.login || ""; - console.log(`Checking assignee trigger: user='${triggerUser}', assignee='${assigneeUsername}'`); + console.log( + `Checking assignee trigger: user='${triggerUser}', assignee='${assigneeUsername}'`, + ); if (triggerUser && assigneeUsername === triggerUser) { console.log(`Issue assigned to trigger user '${triggerUser}'`);