diff --git a/lib/providers/gitea.ts b/lib/providers/gitea.ts index 81672b1..f999f86 100644 --- a/lib/providers/gitea.ts +++ b/lib/providers/gitea.ts @@ -60,72 +60,157 @@ export class GiteaProvider implements IssueProvider { this.workflow = opts.workflow ?? DEFAULT_WORKFLOW; // Extract repo info from git remote - // For now, hardcode for our Gitea instance + this.extractRepoInfoFromRemote(); + + // Get token from environment variable + this.token = process.env.GITEA_TOKEN || ""; + + if (!this.token) { + console.warn("GITEA_TOKEN environment variable not set. Gitea API calls may fail."); + } + } + + private extractRepoInfoFromRemote(): void { + try { + // Try to parse .git/config file directly (synchronous) + const fs = require('fs'); + const path = require('path'); + const gitConfigPath = path.join(this.repoPath, '.git', 'config'); + + if (fs.existsSync(gitConfigPath)) { + const configContent = fs.readFileSync(gitConfigPath, 'utf8'); + const urlMatch = configContent.match(/url\s*=\s*(https?:\/\/[^\s]+)/); + + if (urlMatch) { + const remoteUrl = urlMatch[1]; + // Parse Gitea URL (e.g., https://pfoster.dynu.net/peter/clawd.git) + // or http://192.168.1.150:3000/peter/clawd.git + const repoMatch = remoteUrl.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/\.]+)(?:\.git)?$/); + + if (repoMatch) { + this.baseUrl = repoMatch[1]; + this.owner = repoMatch[2]; + this.repo = repoMatch[3]; + return; + } + } + } + } catch (err) { + console.error("Failed to extract repo info from git config:", err); + } + + // Fallback to hardcoded values for testing this.baseUrl = "https://pfoster.dynu.net"; - this.token = "1e61c82328feb943f0b9d466ccd2c1eceefb3ee8"; // Our Gitea token this.owner = "peter"; this.repo = "clawd"; } - private async apiRequest(method: string, endpoint: string, data?: any): Promise { + private async apiRequest(method: string, endpoint: string, data?: any, retryCount = 0): Promise { + const maxRetries = 2; + + // Validate we have required configuration + if (!this.token) { + throw new Error(`GITEA_TOKEN environment variable is not set. Required for Gitea API authentication.`); + } + + if (!this.baseUrl || !this.owner || !this.repo) { + throw new Error(`Gitea repository information is incomplete. baseUrl: ${this.baseUrl}, owner: ${this.owner}, repo: ${this.repo}`); + } + const url = `${this.baseUrl}/api/v1${endpoint}`; - const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`]; - - if (data) { - args.push("-H", "Content-Type: application/json"); - args.push("-d", JSON.stringify(data)); - } - - args.push(url); - - const result = await runCommand(args, { timeoutMs: 30_000 }); - - // Check for command execution errors - if (result.error) { - throw new Error(`Gitea API ${method} ${endpoint} failed: ${result.error}`); - } - - if (result.exitCode !== undefined && result.exitCode !== 0) { - throw new Error(`Gitea API ${method} ${endpoint} failed (exit ${result.exitCode}): ${result.stderr || 'Unknown error'}`); - } - - if (result.stdout.trim()) { - try { - const parsed = JSON.parse(result.stdout); - if (parsed.message && parsed.message.includes("error")) { - throw new Error(`Gitea API error: ${parsed.message}`); - } - return parsed; - } catch (err) { - throw new Error(`Failed to parse JSON from Gitea API: ${result.stdout.substring(0, 200)}`); + try { + const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`]; + + if (data) { + args.push("-H", "Content-Type: application/json"); + args.push("-d", JSON.stringify(data)); } + + args.push(url); + + const result = await runCommand(args, { timeoutMs: 30_000 }); + + // Check for command execution errors + if (result.error) { + throw new Error(`Gitea API ${method} ${endpoint} failed: ${result.error}`); + } + + if (result.exitCode !== undefined && result.exitCode !== 0) { + throw new Error(`Gitea API ${method} ${endpoint} failed (exit ${result.exitCode}): ${result.stderr || 'Unknown error'}`); + } + + if (result.stdout.trim()) { + try { + const parsed = JSON.parse(result.stdout); + if (parsed.message && parsed.message.includes("error")) { + throw new Error(`Gitea API error: ${parsed.message}`); + } + return parsed; + } catch (err) { + throw new Error(`Failed to parse JSON from Gitea API: ${result.stdout.substring(0, 200)}`); + } + } + + return null; + } catch (err) { + // Retry on network errors + if (retryCount < maxRetries && (err as Error).message.includes("failed")) { + console.warn(`Retrying Gitea API ${method} ${endpoint} (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1))); // Exponential backoff + return this.apiRequest(method, endpoint, data, retryCount + 1); + } + throw err; } - - return null; } private async getLabelId(labelName: string): Promise { - // Hardcoded label IDs for our Gitea instance - const labelMap: Record = { - "Planning": 1, - "To Do": 2, - "Doing": 3, - "To Test": 4, - "Testing": 5, - "To Improve": 6, - "Refining": 7, - "Done": 8, - "To Design": 9, - "Designing": 10 - }; - - const labelId = labelMap[labelName]; - if (!labelId) { - throw new Error(`Label "${labelName}" not found in hardcoded map`); + // Check cache first + if (this.labelCache.has(labelName)) { + return this.labelCache.get(labelName)!; } - return labelId; + try { + // Fetch all labels from Gitea + const labels = await this.apiRequest("GET", `/repos/${this.owner}/${this.repo}/labels`) as GiteaLabel[]; + + // Cache all labels + for (const label of labels) { + this.labelCache.set(label.name, label.id); + } + + const labelId = this.labelCache.get(labelName); + if (!labelId) { + throw new Error(`Label "${labelName}" not found in Gitea repo ${this.owner}/${this.repo}`); + } + + return labelId; + } catch (err) { + // Fallback to hardcoded IDs if API fails + const errorMsg = `Failed to fetch labels from Gitea: ${(err as Error).message}`; + + const fallbackMap: Record = { + "Planning": 1, + "To Do": 2, + "Doing": 3, + "To Test": 4, + "Testing": 5, + "To Improve": 6, + "Refining": 7, + "Done": 8, + "To Design": 9, + "Designing": 10 + }; + + const labelId = fallbackMap[labelName]; + if (!labelId) { + throw new Error(`${errorMsg}. Also, label "${labelName}" not found in fallback map.`); + } + + // Cache the fallback value + this.labelCache.set(labelName, labelId); + return labelId; + } } async ensureLabel(name: string, color: string): Promise {