make webhook

This commit is contained in:
2026-07-13 18:50:38 +02:00
parent 2f128550fc
commit 40c822d3bf
67 changed files with 6286 additions and 2053 deletions
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env node
import { randomUUID } from "node:crypto";
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import { AgentStore } from "../../adapters/database/store.js";
import { GiteaClient } from "../../adapters/gitea/client/client.js";
import {
actorPolicy,
readSecret,
repositoryParts,
validateServerUrl,
} from "../../core/config.js";
import { formatError, requireEnv } from "../../core/contracts.js";
import { type PublicationContext, safeFailure } from "../publication/status.js";
import { log, pumpDeliveries, pumpOutbox } from "./handlers/workers.js";
import { handleHttp } from "./server.js";
async function main(): Promise<void> {
const serverUrl = validateServerUrl(requireEnv("GITEA_SERVER_URL"));
const repository = repositoryParts();
const writeToken = await readSecret("GITEA_WRITE_TOKEN");
const webhookSecret = await readSecret("GITEA_WEBHOOK_SECRET");
const botLogin = requireEnv("CI_AGENT_BOT_LOGIN");
const policy = actorPolicy();
const store = new AgentStore(
process.env.AGENT_DB_PATH || "/var/lib/olixero-agent/agent.db",
);
const owner = `controller-${randomUUID()}`;
if (!store.acquireServiceLock("controller", owner, 30_000))
throw new Error("Another controller owns the service lock");
store.recoverControllerWork();
store.purgeDeliveries(Date.now() - 7 * 24 * 60 * 60_000);
const shutdown = new AbortController();
const client = new GiteaClient(
serverUrl,
writeToken,
repository.owner,
repository.repo,
shutdown.signal,
);
const [configuredRepository, bot] = await Promise.all([
client.getRepository(),
client.getCurrentUser(),
]);
if (bot.login.toLowerCase() !== botLogin.toLowerCase())
throw new Error(`Bot login ${botLogin} does not match ${bot.login}`);
const context: PublicationContext = {
client,
botLogin,
serverUrl,
repository,
repositoryId: configuredRepository.id,
writeToken,
signal: shutdown.signal,
isCancelled: (jobId) => store.isCancelRequested(jobId),
};
let delivering: Promise<void> | undefined;
let publishing: Promise<void> | undefined;
const pump = () => {
if (shutdown.signal.aborted) return;
if (!delivering) {
delivering = pumpDeliveries(
store,
context,
configuredRepository.id,
configuredRepository.full_name,
bot.id,
policy,
)
.catch((error) =>
console.error(
log("error", "Delivery work failed", {
error: safeFailure(error),
}),
),
)
.finally(() => {
delivering = undefined;
});
}
if (!publishing) {
publishing = pumpOutbox(store, context)
.catch((error) =>
console.error(
log("error", "Publication work failed", {
error: safeFailure(error),
}),
),
)
.finally(() => {
publishing = undefined;
});
}
};
const pumpTimer = setInterval(pump, 250);
const lockTimer = setInterval(() => renewLock(store, owner), 5_000);
const retentionTimer = setInterval(
() => store.purgeDeliveries(Date.now() - 7 * 24 * 60 * 60_000),
60 * 60_000,
);
pumpTimer.unref();
lockTimer.unref();
retentionTimer.unref();
const server = createServer((request, response) => {
handleHttp(request, response, {
store,
webhookSecret,
repositoryId: configuredRepository.id,
repositoryFullName: configuredRepository.full_name,
}).catch((error) => {
console.error(
log("error", "Webhook request failed", {
error: safeFailure(error),
}),
);
if (!response.headersSent) response.writeHead(500);
response.end();
});
});
server.headersTimeout = 10_000;
server.requestTimeout = 15_000;
server.keepAliveTimeout = 5_000;
const host = process.env.AGENT_HTTP_HOST || "0.0.0.0";
const port = parsePort(process.env.AGENT_HTTP_PORT || "8080");
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, resolve);
});
console.log(
log("info", "Controller ready", {
host,
port,
repository: configuredRepository.full_name,
}),
);
pump();
await new Promise<void>((resolve) => {
let stopping = false;
const stop = () => {
if (stopping) return;
stopping = true;
clearInterval(pumpTimer);
clearInterval(lockTimer);
clearInterval(retentionTimer);
server.close(() => resolve());
shutdown.abort(new Error("Controller is shutting down"));
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
});
await Promise.all([
delivering?.catch(() => undefined),
publishing?.catch(() => undefined),
]);
store.releaseServiceLock("controller", owner);
store.close();
}
function renewLock(store: AgentStore, owner: string): void {
try {
if (!store.renewServiceLock("controller", owner, 30_000))
process.kill(process.pid, "SIGTERM");
} catch (error) {
console.error(
log("error", "Controller lock renewal failed", {
error: safeFailure(error),
}),
);
process.kill(process.pid, "SIGTERM");
}
}
function parsePort(value: string): number {
const port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65_535)
throw new Error(`Invalid AGENT_HTTP_PORT: ${value}`);
return port;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
console.error(formatError(error));
process.exitCode = 1;
});
}