feat(triggers): start alert workflow after consecutive cron/http failures

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-16 23:45:45 +00:00
co-authored by nsrb
parent 989bf5d487
commit 0c416506d6
8 changed files with 380 additions and 8 deletions
+89 -1
View File
@@ -31,6 +31,10 @@ import {
sendSuccessPage,
} from "./http-trigger-auth.js";
import { resolveConfigRefs } from "./config-refs.js";
import {
buildFailureAlertData,
resolveFailureTriggerConfig,
} from "./trigger-failure.js";
/**
* @typedef {{ owner: string, file: string, workflow: any }} WorkflowEntry
@@ -634,6 +638,76 @@ export function createRegistry(server) {
}
}
/**
* @param {{
* key: string,
* owner: string,
* workflow: Record<string, unknown>,
* runId: string,
* trigger: { type: string, detail?: string | null },
* error: string,
* depth: number,
* }} opts
*/
async function maybeTriggerFailureWorkflow(opts) {
const failureConfig = resolveFailureTriggerConfig(
opts.workflow,
opts.owner,
opts.trigger,
);
if (!failureConfig) return;
const consecutiveFailures = await store.countConsecutiveFailures(
opts.key,
opts.trigger.type,
opts.trigger.detail,
);
if (consecutiveFailures !== failureConfig.threshold) {
log.debug(
{
workflow: opts.key,
consecutiveFailures,
threshold: failureConfig.threshold,
},
"failure alert threshold not reached",
);
return;
}
const destKey = resolveWorkflowTriggerKey(opts.owner, failureConfig.workflowName);
const alertData = buildFailureAlertData({
sourceKey: opts.key,
sourceName:
typeof opts.workflow.name === "string" ? opts.workflow.name : null,
owner: opts.owner,
trigger: opts.trigger,
consecutiveFailures,
runId: opts.runId,
error: opts.error,
});
log.warn(
{
workflow: opts.key,
consecutiveFailures,
triggerWorkflow: failureConfig.workflowName,
destination: destKey,
},
"triggering failure alert workflow",
);
await runWorkflow(
destKey,
{ data: alertData },
{ type: "workflow", detail: `failure:${opts.key}` },
{
parentRunId: opts.runId,
depth: opts.depth + 1,
detach: true,
},
);
}
/**
* @param {string} key
* @param {{ data?: unknown, context?: unknown }} context
@@ -705,11 +779,25 @@ export function createRegistry(server) {
return { runId: run.id, status: "success", result: storedEnvelope(ctx) };
} catch (err) {
runLog.error({ err }, "workflow failed");
const error = err instanceof Error ? err.message : String(err);
await store.finishRun(run.id, "failed", null, err);
try {
await maybeTriggerFailureWorkflow({
key,
owner,
workflow,
runId: run.id,
trigger,
error,
depth,
});
} catch (alertErr) {
runLog.error({ err: alertErr }, "failed to trigger failure alert workflow");
}
return {
runId: run.id,
status: "failed",
error: err instanceof Error ? err.message : String(err),
error,
};
}
};