diff --git a/apps/silo/src/apps/github/workers/github/event-handlers/issue-comment.handler.ts b/apps/silo/src/apps/github/workers/github/event-handlers/issue-comment.handler.ts index 133377cb1c..d0170f463b 100644 --- a/apps/silo/src/apps/github/workers/github/event-handlers/issue-comment.handler.ts +++ b/apps/silo/src/apps/github/workers/github/event-handlers/issue-comment.handler.ts @@ -22,15 +22,16 @@ export type GithubCommentWebhookPayload = GithubWebhookPayload[ }; export const handleIssueComment = async (store: Store, action: GithubCommentAction, data: unknown) => { + // Check if this webhook was triggered by our own Plane->GitHub sync (loop prevention) // @ts-expect-error if (data && data.comment && data.comment.id) { // @ts-expect-error - const exist = await store.get(`silo:comment:${data.comment.id}`); + const exist = await store.get(`silo:comment:gh:${data.comment.id}`); if (exist) { - logger.info(`[ISSUE-COMMENT] Event Processed Successfully, confirmed by target`); - // Remove the webhook from the store + logger.info(`[ISSUE-COMMENT] Event triggered by Plane->GitHub sync, skipping to prevent loop`); + // Remove the key so future legitimate webhooks are not blocked // @ts-expect-error - await store.del(`silo:comment:${data.comment.id}`); + await store.del(`silo:comment:gh:${data.comment.id}`); return true; } } @@ -169,7 +170,9 @@ export const syncCommentWithPlane = async ( comment.id, planeComment ); - await store.set(`silo:comment:${comment.id}`, "true"); + // Set key with Plane comment ID so Plane->GitHub handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await store.set(`silo:comment:plane:${comment.id}`, "true", 5); } else { const createdComment = await planeClient.issueComment.create( workspaceConnection.workspace_slug, @@ -177,7 +180,9 @@ export const syncCommentWithPlane = async ( issue.id, planeComment ); - await store.set(`silo:comment:${createdComment.id}`, "true"); + // Set key with Plane comment ID so Plane->GitHub handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await store.set(`silo:comment:plane:${createdComment.id}`, "true", 5); } }; diff --git a/apps/silo/src/apps/github/workers/github/event-handlers/issue.handler.ts b/apps/silo/src/apps/github/workers/github/event-handlers/issue.handler.ts index 43a35aef3d..30b9b0dec4 100644 --- a/apps/silo/src/apps/github/workers/github/event-handlers/issue.handler.ts +++ b/apps/silo/src/apps/github/workers/github/event-handlers/issue.handler.ts @@ -18,16 +18,17 @@ import type { Store } from "@/worker/base"; const SYNC_LABEL = "plane"; export const handleIssueEvents = async (store: Store, action: string, data: unknown) => { - // If the issue number exist inside the store, skip it + // Check if this webhook was triggered by our own Plane->GitHub sync (loop prevention) + // The Plane->GitHub handler sets a temporary key right after syncing to GitHub // @ts-expect-error if (data && data.issueNumber) { // @ts-expect-error - const exist = await store.get(`silo:issue:${data.issueNumber}`); + const exist = await store.get(`silo:issue:gh:${data.issueNumber}`); if (exist) { - logger.info(`[GITHUB][ISSUE] Event Processed Successfully, confirmed by target`); - // Remove the webhook from the store + logger.info(`[GITHUB][ISSUE] Event triggered by Plane->GitHub sync, skipping to prevent loop`); + // Remove the key so future legitimate webhooks are not blocked // @ts-expect-error - await store.del(`silo:issue:${data.issueNumber}`); + await store.del(`silo:issue:gh:${data.issueNumber}`); return true; } } @@ -205,7 +206,9 @@ export const syncIssueWithPlane = async (store: Store, action: IssueWebhookActio issue.id, planeIssue ); - await store.set(`silo:issue:${issue.id}`, "true"); + // Set key with Plane issue ID so Plane->GitHub handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await store.set(`silo:issue:plane:${issue.id}`, "true", 5); } else { const createdIssue = await planeClient.issue.create( entityConnection.workspace_slug, @@ -241,7 +244,9 @@ export const syncIssueWithPlane = async (store: Store, action: IssueWebhookActio await ghService.createIssueComment(data.owner, data.repositoryName, Number(data.issueNumber), comment); }; - await Promise.all([createLink(), createLinkBack(), store.set(`silo:issue:${createdIssue.id}`, "true")]); + // Set key with Plane issue ID so Plane->GitHub handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await Promise.all([createLink(), createLinkBack(), store.set(`silo:issue:plane:${createdIssue.id}`, "true", 5)]); } } catch (error) { logger.error("Error syncing issue with Plane", error); diff --git a/apps/silo/src/apps/github/workers/plane/event-handlers/issue-comment.handler.ts b/apps/silo/src/apps/github/workers/plane/event-handlers/issue-comment.handler.ts index 968dc0a538..8d24068f5a 100644 --- a/apps/silo/src/apps/github/workers/plane/event-handlers/issue-comment.handler.ts +++ b/apps/silo/src/apps/github/workers/plane/event-handlers/issue-comment.handler.ts @@ -23,14 +23,13 @@ export const handleIssueCommentWebhook = async ( ) => { const payloadId = payload?.id ?? ""; - // Store a key associated with the data in the store - // If the key is present, then we need to requeue all that task and process them again + // Check if this webhook was triggered by our own GitHub->Plane sync (loop prevention) if (payloadId) { - const exist = await store.get(`silo:comment:${payload.id}`); + const exist = await store.get(`silo:comment:plane:${payload.id}`); if (exist) { - logger.info("[PLANE][COMMENT] Event Processed Successfully, confirmed by target"); - // Remove the webhook from the store - await store.del(`silo:comment:${payload.id}`); + logger.info("[PLANE][COMMENT] Event triggered by GitHub->Plane sync, skipping to prevent loop"); + // Remove the key so future legitimate webhooks are not blocked + await store.del(`silo:comment:plane:${payload.id}`); return true; } } @@ -130,7 +129,9 @@ const handleCommentSync = async (store: Store, payload: PlaneWebhookPayload) => credentials, ghIntegrationKey ); - await store.set(`silo:comment:${githubComment.data.id}`, "true"); + // Set key with GitHub comment ID so GitHub->Plane handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await store.set(`silo:comment:gh:${githubComment.data.id}`, "true", 5); if ( !comment.external_id || diff --git a/apps/silo/src/apps/github/workers/plane/event-handlers/issue.handler.ts b/apps/silo/src/apps/github/workers/plane/event-handlers/issue.handler.ts index 101d8d2b6a..5a684fb165 100644 --- a/apps/silo/src/apps/github/workers/plane/event-handlers/issue.handler.ts +++ b/apps/silo/src/apps/github/workers/plane/event-handlers/issue.handler.ts @@ -20,13 +20,14 @@ const apiClient = getAPIClient(); export const imagePrefix = encodeURI(env.SILO_API_BASE_URL + env.SILO_BASE_PATH + "/api/assets/github/"); export const handleIssueWebhook = async (headers: TaskHeaders, mq: MQ, store: Store, payload: PlaneWebhookPayload) => { - // Check for the key in the store, if the key is present, then the issue is already synced + // Check if this webhook was triggered by our own GitHub->Plane sync (loop prevention) + // The GitHub->Plane handler sets a temporary key with the Plane issue ID if (payload && payload.id) { - const exist = await store.get(`silo:issue:${payload.id}`); + const exist = await store.get(`silo:issue:plane:${payload.id}`); if (exist) { - logger.info("[PLANE][ISSUE] Event Processed Successfully, confirmed by target"); - // Remove the webhook from the store - await store.del(`silo:issue:${payload.id}`); + logger.info("[PLANE][ISSUE] Event triggered by GitHub->Plane sync, skipping to prevent loop"); + // Remove the key so future legitimate webhooks are not blocked + await store.del(`silo:issue:plane:${payload.id}`); return true; } } @@ -127,8 +128,9 @@ const handleIssueSync = async (store: Store, payload: PlaneWebhookPayload) => { await Promise.all([addExternalId(), createLink()]); } - // Add the issue number to the store - await store.set(`silo:issue:${githubIssue?.data.number}`, "true"); + // Set key with GitHub issue number so GitHub->Plane handler can detect and skip + // Use 5 second TTL to allow the webhook loop back but expire quickly + await store.set(`silo:issue:gh:${githubIssue?.data.number}`, "true", 5); } catch (error) { logger.error("[Plane][Github] Error handling issue create/update event", { error,