Skip to content

Commit 7db4ca8

Browse files
committed
fix(triggers): clean up GitLab and PagerDuty webhooks on failed setup
Extend the orphan-prevention fix to the remaining providers. When a create call succeeds but post-create validation fails, the created webhook is now deleted before throwing: - GitLab: if the create response can't be parsed for its hook id, the hook is located by its URL and deleted. - PagerDuty: if the subscription response lacks an id or signing secret, the subscription is deleted (by id when known, otherwise located by URL). Both cleanups are best-effort and never throw.
1 parent 3c6cdd5 commit 7db4ca8

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

apps/sim/lib/webhooks/providers/gitlab.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@ function gitlabProjectHooksUrl(projectId: string): string {
2626
return `${GITLAB_API_BASE}/projects/${encodeURIComponent(projectId)}/hooks`
2727
}
2828

29+
/**
30+
* Best-effort cleanup that deletes any project hook pointing at `url`. Used to
31+
* avoid orphaning a hook when the create response can't be parsed for its id.
32+
*/
33+
async function cleanupGitLabHookByUrl(
34+
projectId: string,
35+
accessToken: string,
36+
url: string
37+
): Promise<void> {
38+
const res = await fetch(gitlabProjectHooksUrl(projectId), {
39+
headers: { 'PRIVATE-TOKEN': accessToken },
40+
}).catch(() => null)
41+
if (!res || !res.ok) return
42+
43+
const hooks = (await res.json().catch(() => null)) as Array<{ id?: number; url?: string }> | null
44+
if (!Array.isArray(hooks)) return
45+
46+
await Promise.all(
47+
hooks
48+
.filter((hook) => hook.url === url && hook.id != null)
49+
.map((hook) =>
50+
fetch(`${gitlabProjectHooksUrl(projectId)}/${hook.id}`, {
51+
method: 'DELETE',
52+
headers: { 'PRIVATE-TOKEN': accessToken },
53+
}).catch(() => null)
54+
)
55+
)
56+
}
57+
2958
export const gitlabHandler: WebhookProviderHandler = {
3059
/**
3160
* GitLab echoes the configured "Secret token" verbatim in the `X-Gitlab-Token`
@@ -119,6 +148,9 @@ export const gitlabHandler: WebhookProviderHandler = {
119148

120149
const created = (await res.json().catch(() => ({}))) as { id?: number | string }
121150
if (created.id === undefined || created.id === null) {
151+
// The hook was created but we can't read its id — delete it by URL so it
152+
// is not orphaned in GitLab.
153+
await cleanupGitLabHookByUrl(projectId, accessToken, getNotificationUrl(ctx.webhook))
122154
throw new Error('GitLab webhook created but no hook ID was returned.')
123155
}
124156

apps/sim/lib/webhooks/providers/pagerduty.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ function asRecord(value: unknown): Record<string, unknown> {
4646
return (value as Record<string, unknown>) || {}
4747
}
4848

49+
/**
50+
* Best-effort cleanup of a webhook subscription after a failed setup. Deletes by
51+
* id when known, otherwise finds the subscription pointing at `url` and deletes
52+
* it, so a created subscription is never orphaned in PagerDuty.
53+
*/
54+
async function cleanupPagerDutySubscription(
55+
apiKey: string,
56+
url: string,
57+
subscriptionId?: string
58+
): Promise<void> {
59+
let id = subscriptionId
60+
if (!id) {
61+
const listRes = await fetch(`${PAGERDUTY_API_BASE}/webhook_subscriptions`, {
62+
headers: pagerdutyHeaders(apiKey),
63+
}).catch(() => null)
64+
if (!listRes || !listRes.ok) return
65+
const body = (await listRes.json().catch(() => null)) as {
66+
webhook_subscriptions?: Array<{ id?: string; delivery_method?: { url?: string } }>
67+
} | null
68+
id = body?.webhook_subscriptions?.find((sub) => sub.delivery_method?.url === url)?.id
69+
}
70+
if (!id) return
71+
await fetch(`${PAGERDUTY_API_BASE}/webhook_subscriptions/${id}`, {
72+
method: 'DELETE',
73+
headers: pagerdutyHeaders(apiKey),
74+
}).catch(() => null)
75+
}
76+
4977
function referenceSummary(
5078
value: unknown
5179
): { id?: unknown; summary?: unknown; html_url?: unknown } | null {
@@ -154,9 +182,13 @@ export const pagerdutyHandler: WebhookProviderHandler = {
154182
const externalId = subscription.id as string | undefined
155183
const secret = asRecord(subscription.delivery_method).secret as string | undefined
156184

157-
if (!externalId)
158-
throw new Error('PagerDuty webhook created but no subscription ID was returned.')
159-
if (!secret) {
185+
// The subscription exists once PagerDuty returns success; if it is missing
186+
// its id or signing secret, delete it so it is not orphaned, then fail.
187+
if (!externalId || !secret) {
188+
await cleanupPagerDutySubscription(apiKey, getNotificationUrl(ctx.webhook), externalId)
189+
if (!externalId) {
190+
throw new Error('PagerDuty webhook created but no subscription ID was returned.')
191+
}
160192
throw new Error('PagerDuty webhook created but no signing secret was returned on creation.')
161193
}
162194

0 commit comments

Comments
 (0)