{"record":{"id":"83270e071bca5e81","repo":"mastra-ai/mastra","slug":"github-integration-is-required-to-retire-webhook-s","errorCode":null,"errorMessage":"GitHub integration is required to retire webhook subscriptions.","messagePattern":"GitHub integration is required to retire webhook subscriptions\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/integrations/github/webhook.ts","lineNumber":541,"sourceCode":"  const target = {\n    installationExternalId: notification.metadata.installationId.toString(),\n    repositoryExternalId: notification.metadata.repositoryId.toString(),\n    changeRequestId: notification.metadata.pullRequestNumber.toString(),\n  };\n  const listSubscriptions =\n    dependencies.listSubscriptions ??\n    ((subscriptionTarget: GithubWebhookPullRequestTarget, options?: { includeTerminal?: boolean }) => {\n      if (!dependencies.github) throw new Error('GitHub integration is required to load webhook subscriptions.');\n      return listPullRequestSubscriptionsForWebhook(\n        subscriptionTarget,\n        options,\n        dependencies.github.integrationStorage,\n      );\n    });\n  const retireSubscription =\n    dependencies.retireSubscription ??\n    ((id: string, status: 'open' | 'closed' | 'merged') => {\n      if (!dependencies.github) throw new Error('GitHub integration is required to retire webhook subscriptions.');\n      return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);\n    });\n  const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === 'reopened' });\n  let delivered = 0;\n  let failed = 0;\n  let skipped = 0;\n\n  for (const subscription of subscriptions) {\n    try {\n      const session = await resolveSubscriptionSession(dependencies.controller, subscription, dependencies.github);\n      // No session means this deployment does not hold the subscribed thread.\n      // That is not a delivery failure, so it must not be retried or counted as\n      // one; the subscription is left untouched because the thread may exist\n      // wherever the subscription was created.\n      if (!session) {\n        skipped += 1;\n        dependencies.onTargetSkipped?.(subscription);\n        continue;","sourceCodeStart":523,"sourceCodeEnd":559,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/integrations/github/webhook.ts#L523-L559","documentation":"The retireSubscription dependency in dispatchGithubWebhook defaults to retirePullRequestSubscription, which writes through dependencies.github.integrationStorage. When the GitHub integration dependency is not provided and no custom retireSubscription override is injected, the closure throws so closed/merged PR events cannot silently leave subscriptions unretired.","triggerScenarios":"A PR 'closed' or 'merged' webhook dispatches and dispatchGithubWebhook needs to retire matching subscriptions, but dependencies.github is undefined and no custom dependencies.retireSubscription was supplied.","commonSituations":"Same wiring omission as the listSubscriptions sibling error: entry point constructed without the GitHub integration; tests with partial dependencies that pass listSubscriptions but exercise the retire path; refactors that made the dependency optional without updating all call sites.","solutions":["Pass dependencies.github to dispatchGithubWebhook so retirement can write to integrationStorage","Provide a custom dependencies.retireSubscription override when operating without the full integration","Require dependencies.github in the type signature so omissions are caught at compile time","Add an early validation in dispatchGithubWebhook that either github or both list/retire overrides exist"],"exampleFix":"// before\nawait dispatchGithubWebhook(prNotification, { controller });\n// after\nawait dispatchGithubWebhook(prNotification, {\n  controller,\n  github: { integrationStorage },\n  retireSubscription: (id, status) => retirePullRequestSubscription(id, status, integrationStorage),\n});","handlingStrategy":"validation","validationCode":"if (!dependencies.github && !dependencies.retireSubscription) {\n  throw new Error('dispatchGithubWebhook requires dependencies.github or a retireSubscription override.');\n}","typeGuard":"function canRetireSubscriptions(deps: { github?: unknown; retireSubscription?: unknown }): boolean {\n  return typeof deps.retireSubscription === 'function' ||\n    (typeof deps.github === 'object' && deps.github !== null);\n}","tryCatchPattern":"try {\n  await dispatchGithubWebhook(notification, deps);\n} catch (err) {\n  if ((err as Error).message.includes('required to retire webhook subscriptions')) {\n    logger.error('Cannot retire PR subscriptions: GitHub integration missing from dispatcher deps');\n    return { delivered: 0, failed: 0, skipped: 0 };\n  }\n  throw err;\n}","preventionTips":["Inject the GitHub integration wherever the webhook dispatcher is constructed","Test the closed/merged PR paths specifically — they are the ones that hit retireSubscription","Fail fast at wiring time rather than at first webhook delivery","Centralize dependency construction for webhooks in one module"],"tags":["github","webhook","dependency-injection","misconfiguration"],"backgroundTag":"missing-dependency","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}