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,16 +60,66 @@ export class GiteaProvider implements IssueProvider {
|
|||||||
this.workflow = opts.workflow ?? DEFAULT_WORKFLOW;
|
this.workflow = opts.workflow ?? DEFAULT_WORKFLOW;
|
||||||
|
|
||||||
// Extract repo info from git remote
|
// 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.baseUrl = "https://pfoster.dynu.net";
|
||||||
this.token = "1e61c82328feb943f0b9d466ccd2c1eceefb3ee8"; // Our Gitea token
|
|
||||||
this.owner = "peter";
|
this.owner = "peter";
|
||||||
this.repo = "clawd";
|
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 url = `${this.baseUrl}/api/v1${endpoint}`;
|
||||||
|
|
||||||
|
try {
|
||||||
const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`];
|
const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`];
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -103,11 +153,43 @@ export class GiteaProvider implements IssueProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLabelId(labelName: string): Promise<number> {
|
private async getLabelId(labelName: string): Promise<number> {
|
||||||
// Hardcoded label IDs for our Gitea instance
|
// Check cache first
|
||||||
const labelMap: Record<string, number> = {
|
if (this.labelCache.has(labelName)) {
|
||||||
|
return this.labelCache.get(labelName)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
"Planning": 1,
|
||||||
"To Do": 2,
|
"To Do": 2,
|
||||||
"Doing": 3,
|
"Doing": 3,
|
||||||
@@ -120,13 +202,16 @@ export class GiteaProvider implements IssueProvider {
|
|||||||
"Designing": 10
|
"Designing": 10
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelId = labelMap[labelName];
|
const labelId = fallbackMap[labelName];
|
||||||
if (!labelId) {
|
if (!labelId) {
|
||||||
throw new Error(`Label "${labelName}" not found in hardcoded map`);
|
throw new Error(`${errorMsg}. Also, label "${labelName}" not found in fallback map.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the fallback value
|
||||||
|
this.labelCache.set(labelName, labelId);
|
||||||
return labelId;
|
return labelId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ensureLabel(name: string, color: string): Promise<void> {
|
async ensureLabel(name: string, color: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user