Make Gitea provider configurable with env vars and dynamic label fetching
- Extract Gitea URL, owner, repo from .git/config (synchronous parsing) - Get authentication token from GITEA_TOKEN environment variable - Dynamic label fetching from Gitea API with fallback to hardcoded IDs - Add retry logic with exponential backoff for network errors - Add validation for required configuration (token, URL, repo info) - Better error messages for troubleshooting Configuration: - Set GITEA_TOKEN environment variable before running OpenClaw - Automatically detects repo from git remote URL - Fallback to hardcoded URL if git parsing fails Known issues: - Label transitions sometimes don't persist (edge case under investigation) - Recommend manual label updates via Gitea UI if transition fails
This commit is contained in:
@@ -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<any> {
|
||||
private async apiRequest(method: string, endpoint: string, data?: any, retryCount = 0): Promise<any> {
|
||||
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<number> {
|
||||
// Hardcoded label IDs for our Gitea instance
|
||||
const labelMap: Record<string, number> = {
|
||||
"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<string, number> = {
|
||||
"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<void> {
|
||||
|
||||
Reference in New Issue
Block a user