Skip to content

Commit 207fa9b

Browse files
committed
fix(webapp,database): reactivate a re-declared webhook, keep operator disables
A webhook removed from the deploy manifest is deactivated by the declarative sync. On a later deploy that re-declares it, the endpoint stayed INACTIVE and silently dropped deliveries. A new WebhookEndpoint.manuallyDeactivatedAt timestamp distinguishes an operator disable from that auto-deactivation: the sync reactivates an auto-deactivated endpoint on re-declare, but leaves an operator-disabled one alone. The disable and enable endpoints set and clear the timestamp.
1 parent 648409d commit 207fa9b

6 files changed

Lines changed: 45 additions & 4 deletions

File tree

apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const { action, loader } = createActionApiRoute(
2525

2626
await webhookPrisma.webhookEndpoint.update({
2727
where: { id: endpoint.id },
28-
data: { status: "INACTIVE" },
28+
data: { status: "INACTIVE", manuallyDeactivatedAt: new Date() },
2929
});
3030
webhookEngine.invalidateEndpoint(endpoint.opaqueId);
3131

apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const { action, loader } = createActionApiRoute(
2525

2626
await webhookPrisma.webhookEndpoint.update({
2727
where: { id: endpoint.id },
28-
data: { status: "ACTIVE" },
28+
data: { status: "ACTIVE", manuallyDeactivatedAt: null },
2929
});
3030
webhookEngine.invalidateEndpoint(endpoint.opaqueId);
3131

apps/webapp/app/v3/services/createBackgroundWorker.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ export async function syncDeclarativeWebhooks(
754754
verifierArtifact: wh.verifierArtifact as unknown as Prisma.InputJsonValue,
755755
secretProvisioning: wh.secretProvisioning ?? "either",
756756
metadata: (wh.metadata ?? {}) as unknown as Prisma.InputJsonValue,
757+
...(found.manuallyDeactivatedAt === null ? { status: "ACTIVE" as const } : {}),
757758
...filterData,
758759
},
759760
});

apps/webapp/test/syncDeclarativeWebhooks.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ async function seedEndpoint(
6464
prisma: PrismaClient,
6565
base: { organizationId: string; projectId: string; runtimeEnvironmentId: string },
6666
handlerWebhookId: string,
67-
status: "ACTIVE" | "INACTIVE"
67+
status: "ACTIVE" | "INACTIVE",
68+
manuallyDeactivatedAt: Date | null = null
6869
) {
6970
const suffix = Math.random().toString(36).slice(2, 10);
7071
return prisma.webhookEndpoint.create({
@@ -80,6 +81,7 @@ async function seedEndpoint(
8081
routingTarget: { type: "task", taskId: "handle-stripe" },
8182
verifierArtifact: { kind: "bundle", bundleUrl: "https://example.test/v.js", hash: "h" },
8283
status,
84+
manuallyDeactivatedAt,
8385
},
8486
});
8587
}
@@ -154,7 +156,8 @@ describe("syncDeclarativeWebhooks status reconciliation", () => {
154156
runtimeEnvironmentId: environment.id,
155157
},
156158
"declared-webhook",
157-
"INACTIVE"
159+
"INACTIVE",
160+
new Date()
158161
);
159162

160163
await syncDeclarativeWebhooks(
@@ -167,6 +170,37 @@ describe("syncDeclarativeWebhooks status reconciliation", () => {
167170

168171
const after = await prisma.webhookEndpoint.findUniqueOrThrow({ where: { id: endpoint.id } });
169172
expect(after.status).toBe("INACTIVE");
173+
expect(after.manuallyDeactivatedAt).not.toBeNull();
174+
}
175+
);
176+
177+
containerTest(
178+
"a redeploy re-activates an endpoint auto-deactivated when it was removed then re-declared",
179+
async ({ prisma }) => {
180+
const { organization, project, environment } = await seedProjectWithEnv(prisma);
181+
const worker = await seedWorkerWithTask(prisma, project, environment, "handle-stripe");
182+
const endpoint = await seedEndpoint(
183+
prisma,
184+
{
185+
organizationId: organization.id,
186+
projectId: project.id,
187+
runtimeEnvironmentId: environment.id,
188+
},
189+
"declared-webhook",
190+
"INACTIVE",
191+
null
192+
);
193+
194+
await syncDeclarativeWebhooks(
195+
[makeWebhookResource("declared-webhook", "handle-stripe")],
196+
worker,
197+
asEnv(environment),
198+
prisma,
199+
prisma
200+
);
201+
202+
const after = await prisma.webhookEndpoint.findUniqueOrThrow({ where: { id: endpoint.id } });
203+
expect(after.status).toBe("ACTIVE");
170204
}
171205
);
172206

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "public"."WebhookEndpoint" ADD COLUMN "manuallyDeactivatedAt" TIMESTAMP(3);

internal-packages/database/prisma/schema.prisma

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,10 @@ model WebhookEndpoint {
799799
signingSecretKey String?
800800
801801
status WebhookEndpointStatus @default(ACTIVE)
802+
/// When an operator disabled the endpoint via the dashboard/API. Null means the declarative sync
803+
/// owns the status: a redeploy that re-declares a previously-removed (auto-deactivated) webhook
804+
/// reactivates it. Non-null means the operator disabled it, so the sync leaves the status alone.
805+
manuallyDeactivatedAt DateTime?
802806
createdAt DateTime @default(now())
803807
updatedAt DateTime @updatedAt
804808

0 commit comments

Comments
 (0)