gateway: add cron finished-run webhook (#14535)

* gateway: add cron finished webhook delivery

* config: allow cron webhook in runtime schema

* cron: require notify flag for webhook posts

* ui/docs: add cron notify toggle and webhook docs

* fix: harden cron webhook auth and fill notify coverage (#14535) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
This commit is contained in:
Advait Paliwal
2026-02-15 16:14:17 -08:00
committed by GitHub
parent ab000bc411
commit 115cfb4430
25 changed files with 519 additions and 4 deletions

View File

@@ -20,6 +20,17 @@ export type GatewayCronState = {
cronEnabled: boolean;
};
const CRON_WEBHOOK_TIMEOUT_MS = 10_000;
function redactWebhookUrl(url: string): string {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return "<invalid-webhook-url>";
}
}
export function buildGatewayCronService(params: {
cfg: ReturnType<typeof loadConfig>;
deps: CliDeps;
@@ -93,6 +104,40 @@ export function buildGatewayCronService(params: {
onEvent: (evt) => {
params.broadcast("cron", evt, { dropIfSlow: true });
if (evt.action === "finished") {
const webhookUrl = params.cfg.cron?.webhook?.trim();
const webhookToken = params.cfg.cron?.webhookToken?.trim();
const job = cron.getJob(evt.jobId);
if (webhookUrl && evt.summary && job?.notify === true) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (webhookToken) {
headers.Authorization = `Bearer ${webhookToken}`;
}
const abortController = new AbortController();
const timeout = setTimeout(() => {
abortController.abort();
}, CRON_WEBHOOK_TIMEOUT_MS);
void fetch(webhookUrl, {
method: "POST",
headers,
body: JSON.stringify(evt),
signal: abortController.signal,
})
.catch((err) => {
cronLogger.warn(
{
err: String(err),
jobId: evt.jobId,
webhookUrl: redactWebhookUrl(webhookUrl),
},
"cron: webhook delivery failed",
);
})
.finally(() => {
clearTimeout(timeout);
});
}
const logPath = resolveCronRunLogPath({
storePath,
jobId: evt.jobId,