Compare commits

..

5 Commits

Author SHA1 Message Date
84c364693c Document Gitea configuration and known limitations 2026-02-15 09:26:53 +00:00
078b3b8174 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
2026-02-15 09:26:37 +00:00
30c4a9a088 Fix Gitea provider label transitions and error handling
- Fix label transition to use PUT /issues/{id}/labels endpoint (PATCH doesn't work)
- Add better error handling in apiRequest method
- Hardcode label IDs for reliability (temporary fix)
- Handle undefined exitCode from runCommand
- Update base URL to use public pfoster.dynu.net
2026-02-15 09:21:03 +00:00
229312323d Update README to document Gitea fork 2026-02-15 09:00:28 +00:00
862c600152 Update Gitea provider to use public URL 2026-02-15 08:59:51 +00:00
2 changed files with 194 additions and 46 deletions

View File

@@ -2,18 +2,75 @@
<img src="assets/DevClaw.png" width="300" alt="DevClaw Logo">
</p>
# DevClaw — Development Plugin for OpenClaw
# DevClaw — Development Plugin for OpenClaw (Gitea Fork)
**Fork with Gitea support for OpenClaw development orchestration.**
> **Note:** This is a fork of the original [DevClaw](https://github.com/laurentenhoor/devclaw) with added **Gitea provider support**. Use this version if you use Gitea instead of GitHub/GitLab.
**Turn any group chat into a dev team that ships.**
DevClaw is a plugin for [OpenClaw](https://openclaw.ai) that turns your orchestrator agent into a development manager. It hires developers, assigns tasks, reviews code, and keeps the pipeline moving — across as many projects as you have group chats.
**Prerequisites:** [OpenClaw](https://openclaw.ai) must be installed and running.
## 🆕 Gitea Support
This fork adds support for **Gitea** issue tracking alongside GitHub and GitLab. The Gitea provider uses the Gitea REST API with token authentication.
### Installation
```bash
openclaw plugins install @laurentenhoor/devclaw
# Install from our Gitea fork
openclaw plugins install https://pfoster.dynu.net/peter/devclaw-gitea.git
# Or clone and install locally
git clone https://pfoster.dynu.net/peter/devclaw-gitea.git
openclaw plugins install -l ./devclaw-gitea
```
### Configuration for Gitea
1. **Set Gitea Token:**
```bash
export GITEA_TOKEN=your_gitea_personal_access_token
```
Or add to systemd override:
```bash
mkdir -p ~/.config/systemd/user/openclaw-gateway.service.d
echo -e '[Service]\nEnvironment="GITEA_TOKEN=your_token"' > ~/.config/systemd/user/openclaw-gateway.service.d/override.conf
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway
```
2. **Automatic Detection:**
- The Gitea provider automatically detects your Gitea instance from git remote URL
- Parse from `.git/config`: `https://your-gitea-domain.com/owner/repo.git`
- Fallback to default if parsing fails
3. **Project Registration:**
When registering a project with DevClaw:
```
/dev register my-project /path/to/repo development
```
The provider auto-detects Gitea and configures accordingly.
### Gitea-Specific Features
- ✅ Auto-creates DevClaw workflow labels (Planning, To Do, Doing, To Test, Testing, To Improve, Refining, Done, To Design, Designing)
- ✅ Dynamic label ID fetching with fallback
- ✅ Issue creation, reading, and label transitions
- ✅ Comment management
- ✅ Retry logic with exponential backoff
- ✅ Clear error messages for troubleshooting
### Known Limitations
- Label transitions sometimes require manual refresh (edge case under investigation)
- Recommend checking Gitea UI if label doesn't update immediately
- PR/MR detection not implemented for Gitea (use issue tracking only)
**Prerequisites:** [OpenClaw](https://openclaw.ai) must be installed and running.
Then start onboarding by chatting with your agent in any channel:
```
"Hey, can you help me set up DevClaw?"

View File

@@ -60,45 +60,108 @@ 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.baseUrl = "http://192.168.1.150:3000";
this.token = "1e61c82328feb943f0b9d466ccd2c1eceefb3ee8"; // Our Gitea token
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.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 });
if (result.stderr) {
console.error(`Gitea API error for ${method} ${endpoint}:`, result.stderr);
}
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) {
console.error(`Failed to parse JSON from Gitea API: ${result.stdout.substring(0, 200)}`);
throw new Error(`Failed to parse JSON from Gitea API: ${(err as Error).message}`);
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> {
@@ -107,19 +170,47 @@ export class GiteaProvider implements IssueProvider {
return this.labelCache.get(labelName)!;
}
// Fetch all labels and cache them
const labels = await this.apiRequest("GET", `/repos/${this.owner}/${this.repo}/labels`) as GiteaLabel[];
for (const label of labels) {
this.labelCache.set(label.name, label.id);
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;
}
const labelId = this.labelCache.get(labelName);
if (!labelId) {
throw new Error(`Label "${labelName}" not found in Gitea repo`);
}
return labelId;
}
async ensureLabel(name: string, color: string): Promise<void> {