@generacy-ai/orchestrator 0.0.0-preview-20260304013206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/LICENSE +191 -0
  2. package/dist/auth/api-key.d.ts +63 -0
  3. package/dist/auth/api-key.d.ts.map +1 -0
  4. package/dist/auth/api-key.js +102 -0
  5. package/dist/auth/api-key.js.map +1 -0
  6. package/dist/auth/github-oauth.d.ts +29 -0
  7. package/dist/auth/github-oauth.d.ts.map +1 -0
  8. package/dist/auth/github-oauth.js +136 -0
  9. package/dist/auth/github-oauth.js.map +1 -0
  10. package/dist/auth/index.d.ts +5 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +9 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/jwt.d.ts +34 -0
  15. package/dist/auth/jwt.d.ts.map +1 -0
  16. package/dist/auth/jwt.js +74 -0
  17. package/dist/auth/jwt.js.map +1 -0
  18. package/dist/auth/middleware.d.ts +43 -0
  19. package/dist/auth/middleware.d.ts.map +1 -0
  20. package/dist/auth/middleware.js +101 -0
  21. package/dist/auth/middleware.js.map +1 -0
  22. package/dist/config/index.d.ts +3 -0
  23. package/dist/config/index.d.ts.map +1 -0
  24. package/dist/config/index.js +5 -0
  25. package/dist/config/index.js.map +1 -0
  26. package/dist/config/loader.d.ts +21 -0
  27. package/dist/config/loader.d.ts.map +1 -0
  28. package/dist/config/loader.js +295 -0
  29. package/dist/config/loader.js.map +1 -0
  30. package/dist/config/schema.d.ts +733 -0
  31. package/dist/config/schema.d.ts.map +1 -0
  32. package/dist/config/schema.js +174 -0
  33. package/dist/config/schema.js.map +1 -0
  34. package/dist/index.d.ts +24 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +33 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/middleware/error-handler.d.ts +30 -0
  39. package/dist/middleware/error-handler.d.ts.map +1 -0
  40. package/dist/middleware/error-handler.js +173 -0
  41. package/dist/middleware/error-handler.js.map +1 -0
  42. package/dist/middleware/index.d.ts +4 -0
  43. package/dist/middleware/index.d.ts.map +1 -0
  44. package/dist/middleware/index.js +4 -0
  45. package/dist/middleware/index.js.map +1 -0
  46. package/dist/middleware/rate-limit.d.ts +32 -0
  47. package/dist/middleware/rate-limit.d.ts.map +1 -0
  48. package/dist/middleware/rate-limit.js +92 -0
  49. package/dist/middleware/rate-limit.js.map +1 -0
  50. package/dist/middleware/request-logger.d.ts +31 -0
  51. package/dist/middleware/request-logger.d.ts.map +1 -0
  52. package/dist/middleware/request-logger.js +89 -0
  53. package/dist/middleware/request-logger.js.map +1 -0
  54. package/dist/routes/agents.d.ts +7 -0
  55. package/dist/routes/agents.d.ts.map +1 -0
  56. package/dist/routes/agents.js +78 -0
  57. package/dist/routes/agents.js.map +1 -0
  58. package/dist/routes/dispatch.d.ts +8 -0
  59. package/dist/routes/dispatch.d.ts.map +1 -0
  60. package/dist/routes/dispatch.js +46 -0
  61. package/dist/routes/dispatch.js.map +1 -0
  62. package/dist/routes/events.d.ts +14 -0
  63. package/dist/routes/events.d.ts.map +1 -0
  64. package/dist/routes/events.js +240 -0
  65. package/dist/routes/events.js.map +1 -0
  66. package/dist/routes/health.d.ts +14 -0
  67. package/dist/routes/health.d.ts.map +1 -0
  68. package/dist/routes/health.js +132 -0
  69. package/dist/routes/health.js.map +1 -0
  70. package/dist/routes/index.d.ts +36 -0
  71. package/dist/routes/index.d.ts.map +1 -0
  72. package/dist/routes/index.js +34 -0
  73. package/dist/routes/index.js.map +1 -0
  74. package/dist/routes/integrations.d.ts +34 -0
  75. package/dist/routes/integrations.d.ts.map +1 -0
  76. package/dist/routes/integrations.js +81 -0
  77. package/dist/routes/integrations.js.map +1 -0
  78. package/dist/routes/metrics.d.ts +31 -0
  79. package/dist/routes/metrics.d.ts.map +1 -0
  80. package/dist/routes/metrics.js +157 -0
  81. package/dist/routes/metrics.js.map +1 -0
  82. package/dist/routes/pr-webhooks.d.ts +15 -0
  83. package/dist/routes/pr-webhooks.d.ts.map +1 -0
  84. package/dist/routes/pr-webhooks.js +114 -0
  85. package/dist/routes/pr-webhooks.js.map +1 -0
  86. package/dist/routes/queue.d.ts +7 -0
  87. package/dist/routes/queue.d.ts.map +1 -0
  88. package/dist/routes/queue.js +94 -0
  89. package/dist/routes/queue.js.map +1 -0
  90. package/dist/routes/webhooks.d.ts +15 -0
  91. package/dist/routes/webhooks.d.ts.map +1 -0
  92. package/dist/routes/webhooks.js +117 -0
  93. package/dist/routes/webhooks.js.map +1 -0
  94. package/dist/routes/workflows.d.ts +7 -0
  95. package/dist/routes/workflows.d.ts.map +1 -0
  96. package/dist/routes/workflows.js +138 -0
  97. package/dist/routes/workflows.js.map +1 -0
  98. package/dist/server.d.ts +35 -0
  99. package/dist/server.d.ts.map +1 -0
  100. package/dist/server.js +264 -0
  101. package/dist/server.js.map +1 -0
  102. package/dist/services/agent-registry.d.ts +89 -0
  103. package/dist/services/agent-registry.d.ts.map +1 -0
  104. package/dist/services/agent-registry.js +173 -0
  105. package/dist/services/agent-registry.js.map +1 -0
  106. package/dist/services/epic-completion-monitor-service.d.ts +69 -0
  107. package/dist/services/epic-completion-monitor-service.d.ts.map +1 -0
  108. package/dist/services/epic-completion-monitor-service.js +200 -0
  109. package/dist/services/epic-completion-monitor-service.js.map +1 -0
  110. package/dist/services/identity.d.ts +41 -0
  111. package/dist/services/identity.d.ts.map +1 -0
  112. package/dist/services/identity.js +91 -0
  113. package/dist/services/identity.js.map +1 -0
  114. package/dist/services/index.d.ts +13 -0
  115. package/dist/services/index.d.ts.map +1 -0
  116. package/dist/services/index.js +13 -0
  117. package/dist/services/index.js.map +1 -0
  118. package/dist/services/label-monitor-service.d.ts +95 -0
  119. package/dist/services/label-monitor-service.d.ts.map +1 -0
  120. package/dist/services/label-monitor-service.js +414 -0
  121. package/dist/services/label-monitor-service.js.map +1 -0
  122. package/dist/services/label-sync-service.d.ts +76 -0
  123. package/dist/services/label-sync-service.d.ts.map +1 -0
  124. package/dist/services/label-sync-service.js +108 -0
  125. package/dist/services/label-sync-service.js.map +1 -0
  126. package/dist/services/phase-tracker-service.d.ts +32 -0
  127. package/dist/services/phase-tracker-service.d.ts.map +1 -0
  128. package/dist/services/phase-tracker-service.js +82 -0
  129. package/dist/services/phase-tracker-service.js.map +1 -0
  130. package/dist/services/pr-feedback-monitor-service.d.ts +86 -0
  131. package/dist/services/pr-feedback-monitor-service.d.ts.map +1 -0
  132. package/dist/services/pr-feedback-monitor-service.js +423 -0
  133. package/dist/services/pr-feedback-monitor-service.js.map +1 -0
  134. package/dist/services/queue-service.d.ts +62 -0
  135. package/dist/services/queue-service.d.ts.map +1 -0
  136. package/dist/services/queue-service.js +147 -0
  137. package/dist/services/queue-service.js.map +1 -0
  138. package/dist/services/redis-queue-adapter.d.ts +35 -0
  139. package/dist/services/redis-queue-adapter.d.ts.map +1 -0
  140. package/dist/services/redis-queue-adapter.js +218 -0
  141. package/dist/services/redis-queue-adapter.js.map +1 -0
  142. package/dist/services/smee-receiver.d.ts +87 -0
  143. package/dist/services/smee-receiver.d.ts.map +1 -0
  144. package/dist/services/smee-receiver.js +225 -0
  145. package/dist/services/smee-receiver.js.map +1 -0
  146. package/dist/services/webhook-setup-service.d.ts +253 -0
  147. package/dist/services/webhook-setup-service.d.ts.map +1 -0
  148. package/dist/services/webhook-setup-service.js +369 -0
  149. package/dist/services/webhook-setup-service.js.map +1 -0
  150. package/dist/services/worker-dispatcher.d.ts +57 -0
  151. package/dist/services/worker-dispatcher.d.ts.map +1 -0
  152. package/dist/services/worker-dispatcher.js +211 -0
  153. package/dist/services/worker-dispatcher.js.map +1 -0
  154. package/dist/services/workflow-service.d.ts +65 -0
  155. package/dist/services/workflow-service.d.ts.map +1 -0
  156. package/dist/services/workflow-service.js +164 -0
  157. package/dist/services/workflow-service.js.map +1 -0
  158. package/dist/sse/events.d.ts +50 -0
  159. package/dist/sse/events.d.ts.map +1 -0
  160. package/dist/sse/events.js +152 -0
  161. package/dist/sse/events.js.map +1 -0
  162. package/dist/sse/index.d.ts +4 -0
  163. package/dist/sse/index.d.ts.map +1 -0
  164. package/dist/sse/index.js +7 -0
  165. package/dist/sse/index.js.map +1 -0
  166. package/dist/sse/stream.d.ts +109 -0
  167. package/dist/sse/stream.d.ts.map +1 -0
  168. package/dist/sse/stream.js +239 -0
  169. package/dist/sse/stream.js.map +1 -0
  170. package/dist/sse/subscriptions.d.ts +91 -0
  171. package/dist/sse/subscriptions.d.ts.map +1 -0
  172. package/dist/sse/subscriptions.js +299 -0
  173. package/dist/sse/subscriptions.js.map +1 -0
  174. package/dist/types/api.d.ts +651 -0
  175. package/dist/types/api.d.ts.map +1 -0
  176. package/dist/types/api.js +169 -0
  177. package/dist/types/api.js.map +1 -0
  178. package/dist/types/index.d.ts +6 -0
  179. package/dist/types/index.d.ts.map +1 -0
  180. package/dist/types/index.js +27 -0
  181. package/dist/types/index.js.map +1 -0
  182. package/dist/types/monitor.d.ts +243 -0
  183. package/dist/types/monitor.d.ts.map +1 -0
  184. package/dist/types/monitor.js +2 -0
  185. package/dist/types/monitor.js.map +1 -0
  186. package/dist/types/problem-details.d.ts +63 -0
  187. package/dist/types/problem-details.d.ts.map +1 -0
  188. package/dist/types/problem-details.js +37 -0
  189. package/dist/types/problem-details.js.map +1 -0
  190. package/dist/types/sse.d.ts +224 -0
  191. package/dist/types/sse.d.ts.map +1 -0
  192. package/dist/types/sse.js +103 -0
  193. package/dist/types/sse.js.map +1 -0
  194. package/dist/types/webhook.d.ts +59 -0
  195. package/dist/types/webhook.d.ts.map +1 -0
  196. package/dist/types/webhook.js +2 -0
  197. package/dist/types/webhook.js.map +1 -0
  198. package/dist/utils/correlation.d.ts +30 -0
  199. package/dist/utils/correlation.d.ts.map +1 -0
  200. package/dist/utils/correlation.js +34 -0
  201. package/dist/utils/correlation.js.map +1 -0
  202. package/dist/utils/index.d.ts +3 -0
  203. package/dist/utils/index.d.ts.map +1 -0
  204. package/dist/utils/index.js +3 -0
  205. package/dist/utils/index.js.map +1 -0
  206. package/dist/utils/shutdown.d.ts +28 -0
  207. package/dist/utils/shutdown.d.ts.map +1 -0
  208. package/dist/utils/shutdown.js +86 -0
  209. package/dist/utils/shutdown.js.map +1 -0
  210. package/dist/worker/claude-cli-worker.d.ts +61 -0
  211. package/dist/worker/claude-cli-worker.d.ts.map +1 -0
  212. package/dist/worker/claude-cli-worker.js +330 -0
  213. package/dist/worker/claude-cli-worker.js.map +1 -0
  214. package/dist/worker/cli-spawner.d.ts +42 -0
  215. package/dist/worker/cli-spawner.d.ts.map +1 -0
  216. package/dist/worker/cli-spawner.js +183 -0
  217. package/dist/worker/cli-spawner.js.map +1 -0
  218. package/dist/worker/config.d.ts +76 -0
  219. package/dist/worker/config.d.ts.map +1 -0
  220. package/dist/worker/config.js +39 -0
  221. package/dist/worker/config.js.map +1 -0
  222. package/dist/worker/epic-post-tasks.d.ts +73 -0
  223. package/dist/worker/epic-post-tasks.d.ts.map +1 -0
  224. package/dist/worker/epic-post-tasks.js +266 -0
  225. package/dist/worker/epic-post-tasks.js.map +1 -0
  226. package/dist/worker/gate-checker.d.ts +22 -0
  227. package/dist/worker/gate-checker.d.ts.map +1 -0
  228. package/dist/worker/gate-checker.js +41 -0
  229. package/dist/worker/gate-checker.js.map +1 -0
  230. package/dist/worker/index.d.ts +13 -0
  231. package/dist/worker/index.d.ts.map +1 -0
  232. package/dist/worker/index.js +19 -0
  233. package/dist/worker/index.js.map +1 -0
  234. package/dist/worker/label-manager.d.ts +89 -0
  235. package/dist/worker/label-manager.d.ts.map +1 -0
  236. package/dist/worker/label-manager.js +200 -0
  237. package/dist/worker/label-manager.js.map +1 -0
  238. package/dist/worker/output-capture.d.ts +58 -0
  239. package/dist/worker/output-capture.d.ts.map +1 -0
  240. package/dist/worker/output-capture.js +153 -0
  241. package/dist/worker/output-capture.js.map +1 -0
  242. package/dist/worker/phase-loop.d.ts +64 -0
  243. package/dist/worker/phase-loop.d.ts.map +1 -0
  244. package/dist/worker/phase-loop.js +185 -0
  245. package/dist/worker/phase-loop.js.map +1 -0
  246. package/dist/worker/phase-resolver.d.ts +62 -0
  247. package/dist/worker/phase-resolver.d.ts.map +1 -0
  248. package/dist/worker/phase-resolver.js +144 -0
  249. package/dist/worker/phase-resolver.js.map +1 -0
  250. package/dist/worker/pr-feedback-handler.d.ts +105 -0
  251. package/dist/worker/pr-feedback-handler.d.ts.map +1 -0
  252. package/dist/worker/pr-feedback-handler.js +391 -0
  253. package/dist/worker/pr-feedback-handler.js.map +1 -0
  254. package/dist/worker/pr-linker.d.ts +69 -0
  255. package/dist/worker/pr-linker.d.ts.map +1 -0
  256. package/dist/worker/pr-linker.js +108 -0
  257. package/dist/worker/pr-linker.js.map +1 -0
  258. package/dist/worker/pr-manager.d.ts +61 -0
  259. package/dist/worker/pr-manager.d.ts.map +1 -0
  260. package/dist/worker/pr-manager.js +140 -0
  261. package/dist/worker/pr-manager.js.map +1 -0
  262. package/dist/worker/repo-checkout.d.ts +63 -0
  263. package/dist/worker/repo-checkout.d.ts.map +1 -0
  264. package/dist/worker/repo-checkout.js +160 -0
  265. package/dist/worker/repo-checkout.js.map +1 -0
  266. package/dist/worker/stage-comment-manager.d.ts +34 -0
  267. package/dist/worker/stage-comment-manager.d.ts.map +1 -0
  268. package/dist/worker/stage-comment-manager.js +114 -0
  269. package/dist/worker/stage-comment-manager.js.map +1 -0
  270. package/dist/worker/types.d.ts +192 -0
  271. package/dist/worker/types.d.ts.map +1 -0
  272. package/dist/worker/types.js +53 -0
  273. package/dist/worker/types.js.map +1 -0
  274. package/package.json +64 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * WebhookSetupService
3
+ *
4
+ * Ensures GitHub webhooks are configured for all monitored repositories when
5
+ * SMEE_CHANNEL_URL is set. Verifies existing webhooks, creates missing ones,
6
+ * and reactivates inactive webhooks on orchestrator startup.
7
+ *
8
+ * This eliminates manual webhook configuration and ensures near-instant label
9
+ * detection via Smee instead of relying on 15-minute polling fallback.
10
+ */
11
+ import { executeCommand } from '@generacy-ai/workflow-engine';
12
+ /**
13
+ * Service for auto-configuring GitHub webhooks for monitored repositories.
14
+ *
15
+ * When SMEE_CHANNEL_URL is configured, this service verifies and creates
16
+ * GitHub webhooks for all monitored repositories on orchestrator startup,
17
+ * ensuring near-instant label detection via Smee instead of 15-minute polling.
18
+ *
19
+ * Features:
20
+ * - Creates missing webhooks pointing to Smee channel URL
21
+ * - Skips existing active webhooks (idempotent)
22
+ * - Reactivates inactive webhooks and merges events
23
+ * - Graceful error handling (per-repo failures don't block startup)
24
+ * - Warns on event mismatches without modifying
25
+ * - Validates Smee URL format (warns on non-smee.io URLs)
26
+ *
27
+ * Error Handling:
28
+ * - 403/404 errors: Logged as warnings, system falls back to polling
29
+ * - Network/API errors: Logged and continue with next repo
30
+ * - All errors are non-fatal and don't block orchestrator startup
31
+ */
32
+ export class WebhookSetupService {
33
+ _logger;
34
+ /**
35
+ * Creates a new WebhookSetupService instance.
36
+ *
37
+ * @param logger - Logger instance for structured logging (Pino/Fastify compatible)
38
+ */
39
+ constructor(logger) {
40
+ this._logger = logger;
41
+ }
42
+ /**
43
+ * Ensure webhooks exist for all repositories.
44
+ *
45
+ * This is the main entry point for webhook setup. For each repository:
46
+ * 1. Lists existing webhooks via GitHub API
47
+ * 2. Checks if a webhook already exists for the Smee channel URL
48
+ * 3. Creates missing webhooks, reactivates inactive ones, or skips active ones
49
+ * 4. Logs results and continues on errors (graceful degradation)
50
+ *
51
+ * Per-repository errors are logged but don't block the overall process,
52
+ * allowing partial success when some repositories fail (e.g., due to
53
+ * insufficient permissions).
54
+ *
55
+ * @param smeeChannelUrl - The Smee channel URL to configure webhooks for
56
+ * (e.g., 'https://smee.io/abc123'). URL is validated
57
+ * and a warning is logged if it doesn't point to smee.io.
58
+ * @param repositories - List of repositories to ensure webhooks for. Each
59
+ * repository is processed sequentially to avoid rate limits.
60
+ * @returns Promise resolving to a summary of webhook setup results, including
61
+ * aggregate counts and detailed per-repository results.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const service = new WebhookSetupService(logger);
66
+ * const summary = await service.ensureWebhooks(
67
+ * 'https://smee.io/abc123',
68
+ * [
69
+ * { owner: 'myorg', repo: 'myrepo' },
70
+ * { owner: 'myorg', repo: 'another-repo' }
71
+ * ]
72
+ * );
73
+ * console.log(`Created: ${summary.created}, Skipped: ${summary.skipped}, Failed: ${summary.failed}`);
74
+ * ```
75
+ *
76
+ * @see {@link WebhookSetupSummary} for the structure of the returned summary
77
+ */
78
+ async ensureWebhooks(smeeChannelUrl, repositories) {
79
+ // Validate Smee URL (warn if not smee.io)
80
+ const normalizedUrl = smeeChannelUrl.toLowerCase();
81
+ if (!normalizedUrl.startsWith('https://smee.io/')) {
82
+ this._logger.warn({ smeeChannelUrl }, 'SMEE_CHANNEL_URL does not point to smee.io — ensure this URL is correct');
83
+ }
84
+ // Initialize counters
85
+ const results = [];
86
+ let created = 0;
87
+ let skipped = 0;
88
+ let reactivated = 0;
89
+ let failed = 0;
90
+ // Process each repository
91
+ for (const repo of repositories) {
92
+ const result = await this._ensureWebhookForRepo(repo.owner, repo.repo, smeeChannelUrl);
93
+ results.push(result);
94
+ // Update counters based on action
95
+ switch (result.action) {
96
+ case 'created':
97
+ created++;
98
+ break;
99
+ case 'skipped':
100
+ skipped++;
101
+ break;
102
+ case 'reactivated':
103
+ reactivated++;
104
+ break;
105
+ case 'failed':
106
+ failed++;
107
+ break;
108
+ }
109
+ }
110
+ // Return summary
111
+ return {
112
+ total: repositories.length,
113
+ created,
114
+ skipped,
115
+ reactivated,
116
+ failed,
117
+ results,
118
+ };
119
+ }
120
+ /**
121
+ * Ensure webhook exists for a single repository.
122
+ *
123
+ * Internal method that handles webhook verification and setup for one repository.
124
+ * All errors are caught and converted to a 'failed' result to allow graceful
125
+ * degradation.
126
+ *
127
+ * Logic flow:
128
+ * 1. List existing webhooks
129
+ * 2. Find webhook matching the Smee URL (case-insensitive)
130
+ * 3. If not found: Create new webhook → 'created'
131
+ * 4. If found and active: Skip → 'skipped' (warn on event mismatch)
132
+ * 5. If found but inactive: Reactivate and merge events → 'reactivated'
133
+ *
134
+ * @param owner - Repository owner (organization or user)
135
+ * @param repo - Repository name
136
+ * @param smeeChannelUrl - The Smee channel URL to configure
137
+ * @returns Promise resolving to the result of webhook setup for this repository
138
+ *
139
+ * @throws Never throws - all errors are caught and returned as 'failed' results
140
+ */
141
+ async _ensureWebhookForRepo(owner, repo, smeeChannelUrl) {
142
+ try {
143
+ // List existing webhooks
144
+ const existingWebhooks = await this._listRepoWebhooks(owner, repo);
145
+ // Check for matching webhook
146
+ const matchingWebhook = this._findMatchingWebhook(existingWebhooks, smeeChannelUrl);
147
+ // No matching webhook - create new one
148
+ if (!matchingWebhook) {
149
+ const webhookId = await this._createRepoWebhook(owner, repo, smeeChannelUrl);
150
+ this._logger.info({ owner, repo, webhookId, action: 'created' }, 'Created new webhook for repository');
151
+ return {
152
+ owner,
153
+ repo,
154
+ action: 'created',
155
+ webhookId,
156
+ };
157
+ }
158
+ // Matching webhook exists and is active
159
+ if (matchingWebhook.active) {
160
+ // Check for event mismatch
161
+ const hasIssuesEvent = matchingWebhook.events.includes('issues');
162
+ if (!hasIssuesEvent) {
163
+ this._logger.warn({
164
+ owner,
165
+ repo,
166
+ webhookId: matchingWebhook.id,
167
+ currentEvents: matchingWebhook.events,
168
+ expectedEvents: ['issues'],
169
+ }, 'Existing webhook has event mismatch - events not updated');
170
+ }
171
+ this._logger.info({ owner, repo, webhookId: matchingWebhook.id, action: 'skipped' }, 'Webhook already exists and is active');
172
+ return {
173
+ owner,
174
+ repo,
175
+ action: 'skipped',
176
+ webhookId: matchingWebhook.id,
177
+ };
178
+ }
179
+ // Matching webhook exists but is inactive - reactivate and merge events
180
+ const mergedEvents = [
181
+ ...new Set([...matchingWebhook.events, 'issues']),
182
+ ];
183
+ await this._updateRepoWebhook(owner, repo, matchingWebhook.id, {
184
+ active: true,
185
+ events: mergedEvents,
186
+ });
187
+ this._logger.info({
188
+ owner,
189
+ repo,
190
+ webhookId: matchingWebhook.id,
191
+ action: 'reactivated',
192
+ events: mergedEvents,
193
+ }, 'Reactivated inactive webhook');
194
+ return {
195
+ owner,
196
+ repo,
197
+ action: 'reactivated',
198
+ webhookId: matchingWebhook.id,
199
+ };
200
+ }
201
+ catch (error) {
202
+ // Handle per-repo errors gracefully
203
+ const errorMessage = String(error);
204
+ // Determine appropriate log level and message based on error type
205
+ if (errorMessage.includes('403') || errorMessage.includes('404')) {
206
+ this._logger.warn({ owner, repo, error: errorMessage }, 'Insufficient permissions to manage webhooks (admin:repo_hook required)');
207
+ }
208
+ else if (errorMessage.includes('500')) {
209
+ this._logger.warn({ owner, repo, error: errorMessage }, 'GitHub API error while managing webhooks');
210
+ }
211
+ else {
212
+ this._logger.warn({ owner, repo, error: errorMessage }, 'Failed to manage webhook for repository');
213
+ }
214
+ return {
215
+ owner,
216
+ repo,
217
+ action: 'failed',
218
+ error: errorMessage,
219
+ };
220
+ }
221
+ }
222
+ /**
223
+ * Find a webhook matching the given URL.
224
+ *
225
+ * Uses case-insensitive URL comparison. No URL normalization is performed -
226
+ * simple string matching after lowercasing both URLs. This means that
227
+ * 'https://smee.io/ABC' and 'https://smee.io/abc' are considered equal,
228
+ * but trailing slashes or different protocols would not match.
229
+ *
230
+ * @param webhooks - Array of webhooks to search through
231
+ * @param targetUrl - The URL to match (typically the Smee channel URL)
232
+ * @returns The first matching webhook, or undefined if none found
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const webhooks = [
237
+ * { id: 1, active: true, config: { url: 'https://smee.io/ABC' }, events: ['issues'] }
238
+ * ];
239
+ * const match = this._findMatchingWebhook(webhooks, 'https://smee.io/abc');
240
+ * // Returns the webhook with id: 1
241
+ * ```
242
+ */
243
+ _findMatchingWebhook(webhooks, targetUrl) {
244
+ const normalizedTargetUrl = targetUrl.toLowerCase();
245
+ return webhooks.find((webhook) => {
246
+ const webhookUrl = webhook.config?.url?.toLowerCase() ?? '';
247
+ return webhookUrl === normalizedTargetUrl;
248
+ });
249
+ }
250
+ /**
251
+ * List webhooks for a repository via GitHub API.
252
+ *
253
+ * Uses the `gh` CLI to call `GET /repos/{owner}/{repo}/hooks`.
254
+ * Errors are logged and an empty array is returned for graceful degradation.
255
+ *
256
+ * @param owner - Repository owner (organization or user)
257
+ * @param repo - Repository name
258
+ * @returns Promise resolving to an array of webhooks (empty array on error)
259
+ *
260
+ * @see {@link https://docs.github.com/en/rest/webhooks/repos#list-repository-webhooks}
261
+ */
262
+ async _listRepoWebhooks(owner, repo) {
263
+ try {
264
+ const result = await executeCommand('gh', [
265
+ 'api',
266
+ `/repos/${owner}/${repo}/hooks`,
267
+ ]);
268
+ if (result.exitCode !== 0) {
269
+ this._logger.warn({ owner, repo, stderr: result.stderr }, 'Failed to list webhooks for repository');
270
+ return [];
271
+ }
272
+ // Parse JSON response
273
+ const parsed = JSON.parse(result.stdout);
274
+ // Validate the response is an array
275
+ if (!Array.isArray(parsed)) {
276
+ this._logger.warn({ owner, repo, response: result.stdout }, 'Unexpected response format when listing webhooks (expected array)');
277
+ return [];
278
+ }
279
+ // Return as GitHubWebhook array (minimal validation - trust GitHub API)
280
+ return parsed;
281
+ }
282
+ catch (error) {
283
+ // Handle parse errors and other exceptions gracefully
284
+ this._logger.warn({ owner, repo, error: String(error) }, 'Error listing webhooks for repository');
285
+ return [];
286
+ }
287
+ }
288
+ /**
289
+ * Create a new webhook for a repository via GitHub API.
290
+ *
291
+ * Uses the `gh` CLI to call `POST /repos/{owner}/{repo}/hooks` with:
292
+ * - `config.url`: The Smee channel URL
293
+ * - `config.content_type`: 'json'
294
+ * - `events`: ['issues']
295
+ * - `active`: true
296
+ *
297
+ * @param owner - Repository owner (organization or user)
298
+ * @param repo - Repository name
299
+ * @param smeeChannelUrl - The Smee channel URL to configure
300
+ * @returns Promise resolving to the webhook ID on success
301
+ * @throws Error if webhook creation fails (caught and logged by caller)
302
+ *
303
+ * @see {@link https://docs.github.com/en/rest/webhooks/repos#create-a-repository-webhook}
304
+ */
305
+ async _createRepoWebhook(owner, repo, smeeChannelUrl) {
306
+ const result = await executeCommand('gh', [
307
+ 'api',
308
+ '-X', 'POST',
309
+ `/repos/${owner}/${repo}/hooks`,
310
+ '-f', `config[url]=${smeeChannelUrl}`,
311
+ '-f', 'config[content_type]=json',
312
+ '-F', 'events[]=issues',
313
+ '-F', 'active=true',
314
+ ]);
315
+ if (result.exitCode !== 0) {
316
+ // Extract error message from stderr for better debugging
317
+ const errorMsg = result.stderr.trim() || 'Unknown error';
318
+ throw new Error(`Failed to create webhook: ${errorMsg}`);
319
+ }
320
+ // Parse response to get webhook ID
321
+ const response = JSON.parse(result.stdout);
322
+ return response.id;
323
+ }
324
+ /**
325
+ * Update an existing webhook (reactivate and/or merge events) via GitHub API.
326
+ *
327
+ * Uses the `gh` CLI to call `PATCH /repos/{owner}/{repo}/hooks/{webhookId}`.
328
+ * When reactivating an inactive webhook, this method also merges the 'issues'
329
+ * event with any existing events to preserve the webhook's original configuration.
330
+ *
331
+ * @param owner - Repository owner (organization or user)
332
+ * @param repo - Repository name
333
+ * @param webhookId - Webhook ID to update
334
+ * @param updates - Updates to apply
335
+ * @param updates.active - Set webhook active status (true to reactivate)
336
+ * @param updates.events - Event types to subscribe to (merged with existing)
337
+ * @returns Promise resolving to true on success
338
+ * @throws Error if webhook update fails (caught and logged by caller)
339
+ *
340
+ * @see {@link https://docs.github.com/en/rest/webhooks/repos#update-a-repository-webhook}
341
+ */
342
+ async _updateRepoWebhook(owner, repo, webhookId, updates) {
343
+ // Build gh api command arguments
344
+ const args = [
345
+ 'api',
346
+ '-X', 'PATCH',
347
+ `/repos/${owner}/${repo}/hooks/${webhookId}`,
348
+ ];
349
+ // Add active flag if specified
350
+ if (updates.active !== undefined) {
351
+ args.push('-F', `active=${updates.active}`);
352
+ }
353
+ // Add events if specified (merge with existing)
354
+ if (updates.events && updates.events.length > 0) {
355
+ // GitHub expects events as an array, so we need to add each event
356
+ updates.events.forEach((event) => {
357
+ args.push('-F', `events[]=${event}`);
358
+ });
359
+ }
360
+ const result = await executeCommand('gh', args);
361
+ if (result.exitCode !== 0) {
362
+ // Extract error message from stderr for better debugging
363
+ const errorMsg = result.stderr.trim() || 'Unknown error';
364
+ throw new Error(`Failed to update webhook: ${errorMsg}`);
365
+ }
366
+ return true;
367
+ }
368
+ }
369
+ //# sourceMappingURL=webhook-setup-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-setup-service.js","sourceRoot":"","sources":["../../src/services/webhook-setup-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { Redis } from 'ioredis';
2
+ import type { QueueManager, WorkerHandler } from '../types/index.js';
3
+ import type { DispatchConfig } from '../config/index.js';
4
+ export type LabelCleanupFn = (owner: string, repo: string, issueNumber: number) => Promise<void>;
5
+ interface Logger {
6
+ info(msg: string): void;
7
+ info(obj: Record<string, unknown>, msg: string): void;
8
+ warn(msg: string): void;
9
+ warn(obj: Record<string, unknown>, msg: string): void;
10
+ error(msg: string): void;
11
+ error(obj: Record<string, unknown>, msg: string): void;
12
+ debug(msg: string): void;
13
+ debug(obj: Record<string, unknown>, msg: string): void;
14
+ }
15
+ /**
16
+ * Worker dispatcher that polls the queue, enforces concurrency limits,
17
+ * manages heartbeats, reaps stale workers, and handles graceful shutdown.
18
+ *
19
+ * Follows the same AbortController pattern as LabelMonitorService.
20
+ */
21
+ export declare class WorkerDispatcher {
22
+ private readonly queue;
23
+ private readonly redis;
24
+ private readonly logger;
25
+ private readonly config;
26
+ private readonly handler;
27
+ private readonly activeWorkers;
28
+ private readonly labelCleanup?;
29
+ private abortController;
30
+ private running;
31
+ constructor(queue: QueueManager, redis: Redis, logger: Logger, config: DispatchConfig, handler: WorkerHandler, labelCleanup?: LabelCleanupFn);
32
+ /**
33
+ * Start the poll loop and reaper loop.
34
+ */
35
+ start(): Promise<void>;
36
+ /**
37
+ * Stop the dispatcher gracefully.
38
+ * Waits for in-flight workers up to shutdownTimeoutMs, then releases remaining.
39
+ */
40
+ stop(): Promise<void>;
41
+ /**
42
+ * Get the number of active workers.
43
+ */
44
+ getActiveWorkerCount(): number;
45
+ /**
46
+ * Check if the dispatcher is running.
47
+ */
48
+ isRunning(): boolean;
49
+ private pollLoop;
50
+ private pollOnce;
51
+ private runWorker;
52
+ private reaperLoop;
53
+ private reapStaleWorkers;
54
+ private sleep;
55
+ }
56
+ export {};
57
+ //# sourceMappingURL=worker-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-dispatcher.d.ts","sourceRoot":"","sources":["../../src/services/worker-dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjG,UAAU,MAAM;IACd,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACxD;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiC;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAiB;IAC/C,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,OAAO,CAAS;gBAGtB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,aAAa,EACtB,YAAY,CAAC,EAAE,cAAc;IAU/B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B5B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C3B;;OAEG;IACH,oBAAoB,IAAI,MAAM;IAI9B;;OAEG;IACH,SAAS,IAAI,OAAO;YAIN,QAAQ;YAYR,QAAQ;YAkDR,SAAS;YA2BT,UAAU;YAcV,gBAAgB;IAsC9B,OAAO,CAAC,KAAK;CAiBd"}
@@ -0,0 +1,211 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Worker dispatcher that polls the queue, enforces concurrency limits,
4
+ * manages heartbeats, reaps stale workers, and handles graceful shutdown.
5
+ *
6
+ * Follows the same AbortController pattern as LabelMonitorService.
7
+ */
8
+ export class WorkerDispatcher {
9
+ queue;
10
+ redis;
11
+ logger;
12
+ config;
13
+ handler;
14
+ activeWorkers = new Map();
15
+ labelCleanup;
16
+ abortController = null;
17
+ running = false;
18
+ constructor(queue, redis, logger, config, handler, labelCleanup) {
19
+ this.queue = queue;
20
+ this.redis = redis;
21
+ this.logger = logger;
22
+ this.config = config;
23
+ this.handler = handler;
24
+ this.labelCleanup = labelCleanup;
25
+ }
26
+ /**
27
+ * Start the poll loop and reaper loop.
28
+ */
29
+ async start() {
30
+ if (this.running) {
31
+ this.logger.warn('Worker dispatcher already running');
32
+ return;
33
+ }
34
+ const ac = new AbortController();
35
+ this.abortController = ac;
36
+ this.running = true;
37
+ this.logger.info({
38
+ pollIntervalMs: this.config.pollIntervalMs,
39
+ maxConcurrentWorkers: this.config.maxConcurrentWorkers,
40
+ heartbeatTtlMs: this.config.heartbeatTtlMs,
41
+ }, 'Starting worker dispatcher');
42
+ // Run poll loop and reaper loop concurrently
43
+ await Promise.all([
44
+ this.pollLoop(ac.signal),
45
+ this.reaperLoop(ac.signal),
46
+ ]);
47
+ this.running = false;
48
+ this.logger.info('Worker dispatcher stopped');
49
+ }
50
+ /**
51
+ * Stop the dispatcher gracefully.
52
+ * Waits for in-flight workers up to shutdownTimeoutMs, then releases remaining.
53
+ */
54
+ async stop() {
55
+ if (!this.abortController)
56
+ return;
57
+ this.logger.info('Stopping worker dispatcher');
58
+ this.abortController.abort();
59
+ this.abortController = null;
60
+ // Wait for in-flight workers with timeout
61
+ if (this.activeWorkers.size > 0) {
62
+ this.logger.info({ activeWorkers: this.activeWorkers.size }, 'Waiting for in-flight workers to complete');
63
+ const workerPromises = Array.from(this.activeWorkers.values()).map(w => w.promise);
64
+ const timeout = new Promise((resolve) => setTimeout(resolve, this.config.shutdownTimeoutMs));
65
+ await Promise.race([
66
+ Promise.allSettled(workerPromises),
67
+ timeout,
68
+ ]);
69
+ // Release any remaining claimed items
70
+ for (const [workerId, worker] of this.activeWorkers) {
71
+ clearInterval(worker.heartbeatInterval);
72
+ try {
73
+ await this.queue.release(workerId, worker.item);
74
+ this.logger.info({ workerId }, 'Released worker item during shutdown');
75
+ }
76
+ catch (error) {
77
+ this.logger.warn({ err: error, workerId }, 'Failed to release worker item during shutdown');
78
+ }
79
+ }
80
+ this.activeWorkers.clear();
81
+ }
82
+ }
83
+ /**
84
+ * Get the number of active workers.
85
+ */
86
+ getActiveWorkerCount() {
87
+ return this.activeWorkers.size;
88
+ }
89
+ /**
90
+ * Check if the dispatcher is running.
91
+ */
92
+ isRunning() {
93
+ return this.running;
94
+ }
95
+ async pollLoop(signal) {
96
+ while (!signal.aborted) {
97
+ try {
98
+ await this.pollOnce();
99
+ }
100
+ catch (error) {
101
+ this.logger.error({ err: error }, 'Error during poll cycle');
102
+ }
103
+ await this.sleep(this.config.pollIntervalMs, signal);
104
+ }
105
+ }
106
+ async pollOnce() {
107
+ // Don't claim if at concurrency limit
108
+ if (this.activeWorkers.size >= this.config.maxConcurrentWorkers) {
109
+ this.logger.debug({ active: this.activeWorkers.size, max: this.config.maxConcurrentWorkers }, 'At concurrency limit, skipping claim');
110
+ return;
111
+ }
112
+ const workerId = randomUUID();
113
+ const item = await this.queue.claim(workerId);
114
+ if (!item) {
115
+ return;
116
+ }
117
+ this.logger.info({ workerId, owner: item.owner, repo: item.repo, issue: item.issueNumber }, 'Dispatching worker for claimed item');
118
+ // Start heartbeat refresh at half the TTL
119
+ const heartbeatKey = `orchestrator:worker:${workerId}:heartbeat`;
120
+ const ttlSeconds = Math.ceil(this.config.heartbeatTtlMs / 1000);
121
+ const heartbeatInterval = setInterval(async () => {
122
+ try {
123
+ await this.redis.set(heartbeatKey, '1', 'EX', ttlSeconds);
124
+ }
125
+ catch (error) {
126
+ this.logger.warn({ err: error, workerId }, 'Failed to refresh heartbeat');
127
+ }
128
+ }, this.config.heartbeatTtlMs / 2);
129
+ // Create the worker promise
130
+ const promise = this.runWorker(workerId, item, heartbeatInterval);
131
+ const workerInfo = {
132
+ workerId,
133
+ item,
134
+ startedAt: Date.now(),
135
+ heartbeatInterval,
136
+ promise,
137
+ };
138
+ this.activeWorkers.set(workerId, workerInfo);
139
+ }
140
+ async runWorker(workerId, item, heartbeatInterval) {
141
+ try {
142
+ await this.handler(item);
143
+ // Success: complete the item
144
+ await this.queue.complete(workerId, item);
145
+ this.logger.info({ workerId, owner: item.owner, repo: item.repo, issue: item.issueNumber }, 'Worker completed successfully');
146
+ }
147
+ catch (error) {
148
+ // Failure: release back to queue
149
+ await this.queue.release(workerId, item);
150
+ this.logger.error({ err: error, workerId, owner: item.owner, repo: item.repo, issue: item.issueNumber }, 'Worker failed, item released back to queue');
151
+ }
152
+ finally {
153
+ clearInterval(heartbeatInterval);
154
+ this.activeWorkers.delete(workerId);
155
+ }
156
+ }
157
+ async reaperLoop(signal) {
158
+ while (!signal.aborted) {
159
+ await this.sleep(this.config.heartbeatCheckIntervalMs, signal);
160
+ if (signal.aborted)
161
+ break;
162
+ try {
163
+ await this.reapStaleWorkers();
164
+ }
165
+ catch (error) {
166
+ this.logger.error({ err: error }, 'Error during reaper cycle');
167
+ }
168
+ }
169
+ }
170
+ async reapStaleWorkers() {
171
+ for (const [workerId, worker] of this.activeWorkers) {
172
+ const heartbeatKey = `orchestrator:worker:${workerId}:heartbeat`;
173
+ try {
174
+ const alive = await this.redis.exists(heartbeatKey);
175
+ if (!alive) {
176
+ this.logger.warn({ workerId, item: `${worker.item.owner}/${worker.item.repo}#${worker.item.issueNumber}` }, 'Reaping stale worker (heartbeat expired)');
177
+ // Clean up labels before releasing queue item
178
+ if (this.labelCleanup) {
179
+ try {
180
+ await this.labelCleanup(worker.item.owner, worker.item.repo, worker.item.issueNumber);
181
+ }
182
+ catch (error) {
183
+ this.logger.warn({ err: error, workerId }, 'Failed to clean up labels during reap (non-fatal)');
184
+ }
185
+ }
186
+ clearInterval(worker.heartbeatInterval);
187
+ await this.queue.release(workerId, worker.item);
188
+ this.activeWorkers.delete(workerId);
189
+ }
190
+ }
191
+ catch (error) {
192
+ this.logger.warn({ err: error, workerId }, 'Error checking worker heartbeat during reap');
193
+ }
194
+ }
195
+ }
196
+ sleep(ms, signal) {
197
+ return new Promise((resolve) => {
198
+ if (signal.aborted) {
199
+ resolve();
200
+ return;
201
+ }
202
+ const timer = setTimeout(resolve, ms);
203
+ const onAbort = () => {
204
+ clearTimeout(timer);
205
+ resolve();
206
+ };
207
+ signal.addEventListener('abort', onAbort, { once: true });
208
+ });
209
+ }
210
+ }
211
+ //# sourceMappingURL=worker-dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-dispatcher.js","sourceRoot":"","sources":["../../src/services/worker-dispatcher.ts"],"names":[],"mappings":"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"}