Archived
make webhook
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user