feat: Gitea webhook with HMAC-SHA256 auth, agent label routing, auto-close issues

Made-with: Cursor
This commit is contained in:
2026-02-26 15:24:21 -08:00
parent 1eadbb6631
commit d3b04fcd22
4 changed files with 48 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { execSync } from 'child_process';
import { createJob, getJob, listJobs, updateJob } from './job-store';
import { runAgent } from './agent-runner';
@@ -136,21 +137,28 @@ app.get('/api/jobs', (req: Request, res: Response) => {
res.json(listJobs(limit));
});
// Gitea webhook endpoint — triggers agent from a push/issue event
app.post('/webhook/gitea', (req: Request, res: Response) => {
// Gitea webhook endpoint — triggers agent from an issue event
// Must use raw body for HMAC verification — register before express.json()
app.post('/webhook/gitea', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
const event = req.headers['x-gitea-event'] as string;
const body = req.body as any;
const rawBody = req.body as Buffer;
// Verify secret if configured
// Verify HMAC-SHA256 signature
const webhookSecret = process.env.WEBHOOK_SECRET;
if (webhookSecret) {
const sig = req.headers['x-gitea-signature'] as string;
if (!sig || sig !== webhookSecret) {
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
.digest('hex');
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
res.status(401).json({ error: 'Invalid webhook signature' });
return;
}
}
const body = JSON.parse(rawBody.toString('utf8'));
let task: string | null = null;
let agentName = 'Coder';
let repo: string | undefined;
@@ -164,13 +172,16 @@ app.post('/webhook/gitea', (req: Request, res: Response) => {
agentName = 'PM';
} else if (labels.includes('agent:marketing')) {
agentName = 'Marketing';
} else {
} else if (labels.includes('agent:coder')) {
agentName = 'Coder';
} else {
// No agent label — ignore
res.json({ ignored: true, reason: 'no agent label on issue' });
return;
}
task = `Resolve this GitHub issue:\n\nTitle: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}`;
task = `You have been assigned to resolve a Gitea issue in the repo ${repo}.\n\nIssue #${issue.number}: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}\n\nWhen done, close the issue by calling gitea_close_issue.`;
} else if (event === 'push') {
// Optionally trigger on push — useful for CI-style automation
res.json({ ignored: true, reason: 'push events not auto-processed' });
return;
} else {