Attempt to make this work

This commit is contained in:
Mark Wylde
2025-05-30 21:00:03 +01:00
parent 80886e1c8e
commit f2f966c77e
15 changed files with 349 additions and 104 deletions

View File

@@ -5,16 +5,19 @@ This document outlines the changes made to migrate from GitHub App authenticatio
## What Changed ## What Changed
### 1. Removed GitHub App Dependencies ### 1. Removed GitHub App Dependencies
- **Before**: Used OIDC token exchange with Anthropic's GitHub App service - **Before**: Used OIDC token exchange with Anthropic's GitHub App service
- **After**: Uses standard `GITHUB_TOKEN` from workflow environment - **After**: Uses standard `GITHUB_TOKEN` from workflow environment
- **Benefit**: No external dependencies, works with any Git provider - **Benefit**: No external dependencies, works with any Git provider
### 2. Self-Contained Implementation ### 2. Self-Contained Implementation
- **Before**: Depended on external `anthropics/claude-code-base-action` - **Before**: Depended on external `anthropics/claude-code-base-action`
- **After**: Includes built-in Claude execution engine - **After**: Includes built-in Claude execution engine
- **Benefit**: Complete control over functionality, no external action dependencies - **Benefit**: Complete control over functionality, no external action dependencies
### 3. Gitea Compatibility ### 3. Gitea Compatibility
- **Before**: GitHub-specific triggers and authentication - **Before**: GitHub-specific triggers and authentication
- **After**: Compatible with Gitea Actions (with some limitations) - **After**: Compatible with Gitea Actions (with some limitations)
- **Benefit**: Works with self-hosted Gitea instances - **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 ## Required Changes for Existing Users
### Workflow Permissions ### Workflow Permissions
Update your workflow permissions: Update your workflow permissions:
```yaml ```yaml
@@ -40,6 +44,7 @@ permissions:
``` ```
### Required Token Input ### Required Token Input
Now required to explicitly provide a GitHub token: Now required to explicitly provide a GitHub token:
```yaml ```yaml
@@ -58,6 +63,7 @@ Now required to explicitly provide a GitHub token:
## Gitea Setup ## Gitea Setup
### 1. Basic Gitea Workflow ### 1. Basic Gitea Workflow
Use the example in `examples/gitea-claude.yml`: Use the example in `examples/gitea-claude.yml`:
```yaml ```yaml
@@ -86,13 +92,14 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Run Claude Assistant - name: Run Claude Assistant
uses: ./ # Adjust path as needed for your Gitea setup uses: ./ # Adjust path as needed for your Gitea setup
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
``` ```
### 2. Gitea Limitations ### 2. Gitea Limitations
Be aware of these Gitea Actions limitations: Be aware of these Gitea Actions limitations:
- **`issue_comment` on PRs**: May not trigger reliably in some Gitea versions - **`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 ### 3. Gitea Workarounds
#### For PR Comments #### For PR Comments
Use `issue_comment` instead of `pull_request_review_comment`: Use `issue_comment` instead of `pull_request_review_comment`:
```yaml ```yaml
on: on:
issue_comment: issue_comment:
types: [created] # This covers both issue and PR comments types: [created] # This covers both issue and PR comments
``` ```
#### For Code Review Comments #### For Code Review Comments
Gitea has limited support for code review comment webhooks. Consider using: Gitea has limited support for code review comment webhooks. Consider using:
- Regular issue comments on PRs - Regular issue comments on PRs
- Manual trigger via issue assignment - Manual trigger via issue assignment
- Custom webhooks (advanced setup) - Custom webhooks (advanced setup)
@@ -122,21 +132,25 @@ Gitea has limited support for code review comment webhooks. Consider using:
## Benefits of Migration ## Benefits of Migration
### 1. Simplified Authentication ### 1. Simplified Authentication
- No OIDC token setup required - No OIDC token setup required
- Uses standard workflow tokens - Uses standard workflow tokens
- Works with custom GitHub tokens - Works with custom GitHub tokens
### 2. Provider Independence ### 2. Provider Independence
- No dependency on Anthropic's GitHub App service - No dependency on Anthropic's GitHub App service
- Works with any Git provider supporting Actions - Works with any Git provider supporting Actions
- Self-contained functionality - Self-contained functionality
### 3. Enhanced Control ### 3. Enhanced Control
- Direct control over Claude execution - Direct control over Claude execution
- Customizable tool management - Customizable tool management
- Easier debugging and modifications - Easier debugging and modifications
### 4. Gitea Support ### 4. Gitea Support
- Compatible with self-hosted Gitea - Compatible with self-hosted Gitea
- Automatic fallback to REST API (no GraphQL dependency) - Automatic fallback to REST API (no GraphQL dependency)
- Simplified permission checking for Gitea environments - Simplified permission checking for Gitea environments
@@ -148,8 +162,10 @@ Gitea has limited support for code review comment webhooks. Consider using:
### Common Issues ### Common Issues
#### 1. Token Permissions #### 1. Token Permissions
**Error**: "GitHub token authentication failed" **Error**: "GitHub token authentication failed"
**Solution**: Ensure workflow has required permissions: **Solution**: Ensure workflow has required permissions:
```yaml ```yaml
permissions: permissions:
contents: write contents: write
@@ -158,36 +174,45 @@ permissions:
``` ```
#### 2. Gitea Trigger Issues #### 2. Gitea Trigger Issues
**Error**: Workflow not triggering on PR comments **Error**: Workflow not triggering on PR comments
**Solution**: Use `issue_comment` instead of `pull_request_review_comment` **Solution**: Use `issue_comment` instead of `pull_request_review_comment`
#### 3. Missing Dependencies #### 3. Missing Dependencies
**Error**: "Module not found" or TypeScript errors **Error**: "Module not found" or TypeScript errors
**Solution**: Run `npm install` or `bun install` to update dependencies **Solution**: Run `npm install` or `bun install` to update dependencies
### Gitea-Specific Issues ### Gitea-Specific Issues
#### 1. Authentication Errors #### 1. Authentication Errors
**Error**: "Failed to check permissions: HttpError: Bad credentials" **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. **Solution**: This is normal in Gitea environments. The action automatically detects Gitea and bypasses GitHub-specific permission checks.
#### 1a. User Profile API Errors #### 1a. User Profile API Errors
**Error**: "Prepare step failed with error: Visit Project" or "GET /users/{username} - 404" **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. **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 #### 2. Limited Event Support
Some GitHub Events may not be fully supported in Gitea. Use basic triggers: Some GitHub Events may not be fully supported in Gitea. Use basic triggers:
- `issue_comment` for comments - `issue_comment` for comments
- `issues` for issue events - `issues` for issue events
- `push` for code changes - `push` for code changes
#### 3. Token Scope Limitations #### 3. Token Scope Limitations
Gitea tokens may have different scope limitations. Ensure your Gitea instance allows: Gitea tokens may have different scope limitations. Ensure your Gitea instance allows:
- Repository write access - Repository write access
- Issue/PR comment creation - Issue/PR comment creation
- Branch creation and updates - Branch creation and updates
#### 4. GraphQL Not Supported #### 4. GraphQL Not Supported
**Error**: GraphQL queries failing **Error**: GraphQL queries failing
**Solution**: The action automatically detects Gitea and uses REST API instead of GraphQL. No manual configuration needed. **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 ## Example Workflows
See the `examples/` directory for complete workflow examples: See the `examples/` directory for complete workflow examples:
- `claude.yml` - Updated GitHub Actions workflow - `claude.yml` - Updated GitHub Actions workflow
- `gitea-claude.yml` - Gitea-compatible workflow - `gitea-claude.yml` - Gitea-compatible workflow

View File

@@ -114,7 +114,7 @@ runs:
USE_BEDROCK: ${{ inputs.use_bedrock }} USE_BEDROCK: ${{ inputs.use_bedrock }}
USE_VERTEX: ${{ inputs.use_vertex }} USE_VERTEX: ${{ inputs.use_vertex }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
# GitHub token for repository access # GitHub token for repository access
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -163,4 +163,3 @@ runs:
echo '```json' >> $GITHUB_STEP_SUMMARY echo '```json' >> $GITHUB_STEP_SUMMARY
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY

View File

@@ -29,9 +29,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Run Claude Assistant - name: Run Claude Assistant
uses: ./ # Use local action (adjust path as needed) uses: ./ # Use local action (adjust path as needed)
with: 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 }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60" timeout_minutes: "60"
trigger_phrase: "@claude" trigger_phrase: "@claude"
@@ -40,4 +40,4 @@ jobs:
You are working in a Gitea environment. Be aware that: You are working in a Gitea environment. Be aware that:
- Some GitHub Actions features may behave differently - Some GitHub Actions features may behave differently
- Focus on core functionality and avoid advanced GitHub-specific features - Focus on core functionality and avoid advanced GitHub-specific features
- Use standard git operations when possible - Use standard git operations when possible

View File

@@ -34,7 +34,9 @@ export class ClaudeExecutor {
private initializeClient() { private initializeClient() {
if (this.config.useBedrock || this.config.useVertex) { 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) { if (!this.config.apiKey) {
@@ -63,11 +65,17 @@ export class ClaudeExecutor {
private parseTools(): { allowed: string[]; disallowed: string[] } { private parseTools(): { allowed: string[]; disallowed: string[] } {
const allowed = this.config.allowedTools 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 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 }; return { allowed, disallowed };
@@ -92,7 +100,9 @@ export class ClaudeExecutor {
const prompt = await this.readPrompt(); const prompt = await this.readPrompt();
const tools = this.parseTools(); 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(`Allowed tools: ${tools.allowed.join(", ") || "none"}`);
console.log(`Disallowed tools: ${tools.disallowed.join(", ") || "none"}`); console.log(`Disallowed tools: ${tools.disallowed.join(", ") || "none"}`);
@@ -122,7 +132,7 @@ export class ClaudeExecutor {
}; };
} catch (error) { } catch (error) {
console.error("Claude execution failed:", error); console.error("Claude execution failed:", error);
const executionFile = this.createExecutionLog(null, String(error)); const executionFile = this.createExecutionLog(null, String(error));
return { return {
@@ -134,7 +144,9 @@ export class ClaudeExecutor {
} }
} }
export async function runClaude(config: ClaudeExecutorConfig): Promise<ClaudeExecutorResult> { export async function runClaude(
config: ClaudeExecutorConfig,
): Promise<ClaudeExecutorResult> {
const executor = new ClaudeExecutor(config); const executor = new ClaudeExecutor(config);
return await executor.execute(); return await executor.execute();
} }

View File

@@ -10,8 +10,12 @@ async function main() {
model: process.env.ANTHROPIC_MODEL || process.env.MODEL, model: process.env.ANTHROPIC_MODEL || process.env.MODEL,
promptFile: process.env.PROMPT_FILE, promptFile: process.env.PROMPT_FILE,
prompt: process.env.PROMPT, prompt: process.env.PROMPT,
maxTurns: process.env.MAX_TURNS ? parseInt(process.env.MAX_TURNS) : undefined, maxTurns: process.env.MAX_TURNS
timeoutMinutes: process.env.TIMEOUT_MINUTES ? parseInt(process.env.TIMEOUT_MINUTES) : 30, ? parseInt(process.env.MAX_TURNS)
: undefined,
timeoutMinutes: process.env.TIMEOUT_MINUTES
? parseInt(process.env.TIMEOUT_MINUTES)
: 30,
mcpConfig: process.env.MCP_CONFIG, mcpConfig: process.env.MCP_CONFIG,
allowedTools: process.env.ALLOWED_TOOLS, allowedTools: process.env.ALLOWED_TOOLS,
disallowedTools: process.env.DISALLOWED_TOOLS, disallowedTools: process.env.DISALLOWED_TOOLS,
@@ -43,4 +47,4 @@ main().catch((error) => {
console.error("Unhandled error:", error); console.error("Unhandled error:", error);
core.setFailed(`Unhandled error: ${error}`); core.setFailed(`Unhandled error: ${error}`);
process.exit(1); process.exit(1);
}); });

View File

@@ -133,7 +133,68 @@ async function run() {
} }
} catch (error) { } catch (error) {
console.error("Error checking for changes in branch:", 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})`;
}
} }
} }
} }

View File

@@ -9,7 +9,7 @@ export type Octokits = {
export function createOctokit(token: string): Octokits { export function createOctokit(token: string): Octokits {
return { return {
rest: new Octokit({ rest: new Octokit({
auth: token, auth: token,
baseUrl: GITHUB_API_URL, baseUrl: GITHUB_API_URL,
}), }),

View File

@@ -45,7 +45,9 @@ export async function fetchGitHubData({
} }
// Check if we're in a Gitea environment (no GraphQL support) // 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 contextData: GitHubPullRequest | GitHubIssue | null = null;
let comments: GitHubComment[] = []; let comments: GitHubComment[] = [];
@@ -56,7 +58,9 @@ export async function fetchGitHubData({
if (isGitea) { if (isGitea) {
// Use REST API for Gitea compatibility // Use REST API for Gitea compatibility
if (isPR) { 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({ const prResponse = await octokits.rest.pulls.get({
owner, owner,
repo, repo,
@@ -87,7 +91,7 @@ export async function fetchGitHubData({
repo, repo,
issue_number: parseInt(prNumber), issue_number: parseInt(prNumber),
}); });
comments = commentsResponse.data.map(comment => ({ comments = commentsResponse.data.map((comment) => ({
id: comment.id.toString(), id: comment.id.toString(),
databaseId: comment.id.toString(), databaseId: comment.id.toString(),
body: comment.body || "", body: comment.body || "",
@@ -106,7 +110,7 @@ export async function fetchGitHubData({
repo, repo,
pull_number: parseInt(prNumber), pull_number: parseInt(prNumber),
}); });
changedFiles = filesResponse.data.map(file => ({ changedFiles = filesResponse.data.map((file) => ({
path: file.filename, path: file.filename,
additions: file.additions || 0, additions: file.additions || 0,
deletions: file.deletions || 0, deletions: file.deletions || 0,
@@ -119,7 +123,9 @@ export async function fetchGitHubData({
reviewData = { nodes: [] }; // Simplified for Gitea reviewData = { nodes: [] }; // Simplified for Gitea
} else { } 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({ const issueResponse = await octokits.rest.issues.get({
owner, owner,
repo, repo,
@@ -142,7 +148,7 @@ export async function fetchGitHubData({
repo, repo,
issue_number: parseInt(prNumber), issue_number: parseInt(prNumber),
}); });
comments = commentsResponse.data.map(comment => ({ comments = commentsResponse.data.map((comment) => ({
id: comment.id.toString(), id: comment.id.toString(),
databaseId: comment.id.toString(), databaseId: comment.id.toString(),
body: comment.body || "", body: comment.body || "",

View File

@@ -34,9 +34,49 @@ export async function checkAndDeleteEmptyBranch(
} }
} catch (error) { } catch (error) {
console.error("Error checking for commits on Claude branch:", 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}`; // For Gitea compatibility, try alternative approach using branches endpoint
branchLink = `\n[View branch](${branchUrl})`; 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}`, ref: `heads/${claudeBranch}`,
}); });
console.log(`✅ Deleted empty branch: ${claudeBranch}`); console.log(`✅ Deleted empty branch: ${claudeBranch}`);
} catch (deleteError) { } catch (deleteError: any) {
console.error(`Failed to delete branch ${claudeBranch}:`, deleteError); 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
} }
} }

View File

@@ -96,7 +96,7 @@ export async function setupBranch(
// Get the SHA of the source branch // Get the SHA of the source branch
// For Gitea, try using the branches endpoint instead of git/refs // For Gitea, try using the branches endpoint instead of git/refs
let currentSHA: string; let currentSHA: string;
try { try {
// First try the GitHub-compatible git.getRef approach // First try the GitHub-compatible git.getRef approach
const sourceBranchRef = await octokits.rest.git.getRef({ const sourceBranchRef = await octokits.rest.git.getRef({
@@ -107,14 +107,16 @@ export async function setupBranch(
currentSHA = sourceBranchRef.data.object.sha; currentSHA = sourceBranchRef.data.object.sha;
} catch (gitRefError: any) { } catch (gitRefError: any) {
// If git/refs fails (like in Gitea), use the branches endpoint // 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({ const branchResponse = await octokits.rest.repos.getBranch({
owner, owner,
repo, repo,
branch: sourceBranch, branch: sourceBranch,
}); });
// Gitea uses commit.id instead of commit.sha // GitHub and Gitea both use commit.sha
currentSHA = branchResponse.data.commit.sha || branchResponse.data.commit.id; currentSHA = branchResponse.data.commit.sha;
} }
console.log(`Current SHA: ${currentSHA}`); console.log(`Current SHA: ${currentSHA}`);
@@ -127,18 +129,22 @@ export async function setupBranch(
ref: `refs/heads/${newBranch}`, ref: `refs/heads/${newBranch}`,
sha: currentSHA, sha: currentSHA,
}); });
console.log(`Successfully created branch via API: ${newBranch}`); console.log(`Successfully created branch via API: ${newBranch}`);
} catch (createRefError: any) { } catch (createRefError: any) {
// If git/refs creation fails (like in Gitea), that's expected // If git/refs creation fails (like in Gitea), that's expected
// We'll create the branch when we push files later // 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(
`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`); 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( console.log(`Branch setup completed for: ${newBranch}`);
`Branch setup completed for: ${newBranch}`,
);
// Set outputs for GitHub Actions // Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch); core.setOutput("CLAUDE_BRANCH", newBranch);

View File

@@ -2,9 +2,6 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
export async function setupGitHubToken(): Promise<string> { export async function setupGitHubToken(): Promise<string> {
try { try {
// Check if GitHub token was provided as override // Check if GitHub token was provided as override
@@ -18,14 +15,16 @@ export async function setupGitHubToken(): Promise<string> {
// Use the standard GITHUB_TOKEN from the workflow environment // Use the standard GITHUB_TOKEN from the workflow environment
const workflowToken = process.env.GITHUB_TOKEN; const workflowToken = process.env.GITHUB_TOKEN;
if (workflowToken) { if (workflowToken) {
console.log("Using workflow GITHUB_TOKEN for authentication"); console.log("Using workflow GITHUB_TOKEN for authentication");
core.setOutput("GITHUB_TOKEN", workflowToken); core.setOutput("GITHUB_TOKEN", workflowToken);
return 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) { } catch (error) {
core.setFailed( 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.`, `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.`,

View File

@@ -84,67 +84,135 @@ export async function downloadCommentImages(
let bodyHtml: string | undefined; let bodyHtml: string | undefined;
// Get the HTML version based on comment type // Get the HTML version based on comment type
// Try with full+json mediaType first (GitHub), fallback to regular API (Gitea)
switch (comment.type) { switch (comment.type) {
case "issue_comment": { case "issue_comment": {
const response = await octokits.rest.issues.getComment({ try {
owner, const response = await octokits.rest.issues.getComment({
repo, owner,
comment_id: parseInt(comment.id), repo,
mediaType: { comment_id: parseInt(comment.id),
format: "full+json", mediaType: {
}, format: "full+json",
}); },
bodyHtml = response.data.body_html; });
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; break;
} }
case "review_comment": { case "review_comment": {
const response = await octokits.rest.pulls.getReviewComment({ try {
owner, const response = await octokits.rest.pulls.getReviewComment({
repo, owner,
comment_id: parseInt(comment.id), repo,
mediaType: { comment_id: parseInt(comment.id),
format: "full+json", mediaType: {
}, format: "full+json",
}); },
bodyHtml = response.data.body_html; });
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; break;
} }
case "review_body": { case "review_body": {
const response = await octokits.rest.pulls.getReview({ try {
owner, const response = await octokits.rest.pulls.getReview({
repo, owner,
pull_number: parseInt(comment.pullNumber), repo,
review_id: parseInt(comment.id), pull_number: parseInt(comment.pullNumber),
mediaType: { review_id: parseInt(comment.id),
format: "full+json", mediaType: {
}, format: "full+json",
}); },
bodyHtml = response.data.body_html; });
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; break;
} }
case "issue_body": { case "issue_body": {
const response = await octokits.rest.issues.get({ try {
owner, const response = await octokits.rest.issues.get({
repo, owner,
issue_number: parseInt(comment.issueNumber), repo,
mediaType: { issue_number: parseInt(comment.issueNumber),
format: "full+json", mediaType: {
}, format: "full+json",
}); },
bodyHtml = response.data.body_html; });
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; break;
} }
case "pr_body": { case "pr_body": {
const response = await octokits.rest.pulls.get({ try {
owner, const response = await octokits.rest.pulls.get({
repo, owner,
pull_number: parseInt(comment.pullNumber), repo,
mediaType: { pull_number: parseInt(comment.pullNumber),
format: "full+json", mediaType: {
}, format: "full+json",
}); },
// Type here seems to be wrong });
bodyHtml = (response.data as any).body_html; // 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; break;
} }
} }

View File

@@ -13,10 +13,14 @@ export async function checkHumanActor(
githubContext: ParsedGitHubContext, githubContext: ParsedGitHubContext,
) { ) {
// Check if we're in a Gitea environment // 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) { 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; return;
} }
@@ -38,9 +42,14 @@ export async function checkHumanActor(
console.log(`Verified human actor: ${githubContext.actor}`); console.log(`Verified human actor: ${githubContext.actor}`);
} catch (error) { } 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 // 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}`,
);
} }
} }

View File

@@ -15,8 +15,10 @@ export async function checkWritePermissions(
const { repository, actor } = context; const { repository, actor } = context;
// For Gitea compatibility, check if we're in a non-GitHub environment // 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) { if (isGitea) {
core.info(`Detected Gitea environment, assuming actor has permissions`); core.info(`Detected Gitea environment, assuming actor has permissions`);
return true; return true;

View File

@@ -15,7 +15,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
inputs: { assigneeTrigger, triggerPhrase, directPrompt }, inputs: { assigneeTrigger, triggerPhrase, directPrompt },
} = context; } = 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 direct prompt is provided, always trigger
if (directPrompt) { if (directPrompt) {
@@ -29,7 +31,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
let triggerUser = assigneeTrigger?.replace(/^@/, "") || ""; let triggerUser = assigneeTrigger?.replace(/^@/, "") || "";
const assigneeUsername = context.payload.issue.assignee?.login || ""; 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) { if (triggerUser && assigneeUsername === triggerUser) {
console.log(`Issue assigned to trigger user '${triggerUser}'`); console.log(`Issue assigned to trigger user '${triggerUser}'`);