@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.
- package/LICENSE +191 -0
- package/dist/auth/api-key.d.ts +63 -0
- package/dist/auth/api-key.d.ts.map +1 -0
- package/dist/auth/api-key.js +102 -0
- package/dist/auth/api-key.js.map +1 -0
- package/dist/auth/github-oauth.d.ts +29 -0
- package/dist/auth/github-oauth.d.ts.map +1 -0
- package/dist/auth/github-oauth.js +136 -0
- package/dist/auth/github-oauth.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/jwt.d.ts +34 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +74 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +43 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +101 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +21 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +295 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +733 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +174 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +30 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +173 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +32 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +92 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +31 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +89 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/routes/agents.d.ts +7 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +78 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/dispatch.d.ts +8 -0
- package/dist/routes/dispatch.d.ts.map +1 -0
- package/dist/routes/dispatch.js +46 -0
- package/dist/routes/dispatch.js.map +1 -0
- package/dist/routes/events.d.ts +14 -0
- package/dist/routes/events.d.ts.map +1 -0
- package/dist/routes/events.js +240 -0
- package/dist/routes/events.js.map +1 -0
- package/dist/routes/health.d.ts +14 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +132 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +36 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +34 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/integrations.d.ts +34 -0
- package/dist/routes/integrations.d.ts.map +1 -0
- package/dist/routes/integrations.js +81 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/metrics.d.ts +31 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +157 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/pr-webhooks.d.ts +15 -0
- package/dist/routes/pr-webhooks.d.ts.map +1 -0
- package/dist/routes/pr-webhooks.js +114 -0
- package/dist/routes/pr-webhooks.js.map +1 -0
- package/dist/routes/queue.d.ts +7 -0
- package/dist/routes/queue.d.ts.map +1 -0
- package/dist/routes/queue.js +94 -0
- package/dist/routes/queue.js.map +1 -0
- package/dist/routes/webhooks.d.ts +15 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +117 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/routes/workflows.d.ts +7 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +138 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +264 -0
- package/dist/server.js.map +1 -0
- package/dist/services/agent-registry.d.ts +89 -0
- package/dist/services/agent-registry.d.ts.map +1 -0
- package/dist/services/agent-registry.js +173 -0
- package/dist/services/agent-registry.js.map +1 -0
- package/dist/services/epic-completion-monitor-service.d.ts +69 -0
- package/dist/services/epic-completion-monitor-service.d.ts.map +1 -0
- package/dist/services/epic-completion-monitor-service.js +200 -0
- package/dist/services/epic-completion-monitor-service.js.map +1 -0
- package/dist/services/identity.d.ts +41 -0
- package/dist/services/identity.d.ts.map +1 -0
- package/dist/services/identity.js +91 -0
- package/dist/services/identity.js.map +1 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +13 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/label-monitor-service.d.ts +95 -0
- package/dist/services/label-monitor-service.d.ts.map +1 -0
- package/dist/services/label-monitor-service.js +414 -0
- package/dist/services/label-monitor-service.js.map +1 -0
- package/dist/services/label-sync-service.d.ts +76 -0
- package/dist/services/label-sync-service.d.ts.map +1 -0
- package/dist/services/label-sync-service.js +108 -0
- package/dist/services/label-sync-service.js.map +1 -0
- package/dist/services/phase-tracker-service.d.ts +32 -0
- package/dist/services/phase-tracker-service.d.ts.map +1 -0
- package/dist/services/phase-tracker-service.js +82 -0
- package/dist/services/phase-tracker-service.js.map +1 -0
- package/dist/services/pr-feedback-monitor-service.d.ts +86 -0
- package/dist/services/pr-feedback-monitor-service.d.ts.map +1 -0
- package/dist/services/pr-feedback-monitor-service.js +423 -0
- package/dist/services/pr-feedback-monitor-service.js.map +1 -0
- package/dist/services/queue-service.d.ts +62 -0
- package/dist/services/queue-service.d.ts.map +1 -0
- package/dist/services/queue-service.js +147 -0
- package/dist/services/queue-service.js.map +1 -0
- package/dist/services/redis-queue-adapter.d.ts +35 -0
- package/dist/services/redis-queue-adapter.d.ts.map +1 -0
- package/dist/services/redis-queue-adapter.js +218 -0
- package/dist/services/redis-queue-adapter.js.map +1 -0
- package/dist/services/smee-receiver.d.ts +87 -0
- package/dist/services/smee-receiver.d.ts.map +1 -0
- package/dist/services/smee-receiver.js +225 -0
- package/dist/services/smee-receiver.js.map +1 -0
- package/dist/services/webhook-setup-service.d.ts +253 -0
- package/dist/services/webhook-setup-service.d.ts.map +1 -0
- package/dist/services/webhook-setup-service.js +369 -0
- package/dist/services/webhook-setup-service.js.map +1 -0
- package/dist/services/worker-dispatcher.d.ts +57 -0
- package/dist/services/worker-dispatcher.d.ts.map +1 -0
- package/dist/services/worker-dispatcher.js +211 -0
- package/dist/services/worker-dispatcher.js.map +1 -0
- package/dist/services/workflow-service.d.ts +65 -0
- package/dist/services/workflow-service.d.ts.map +1 -0
- package/dist/services/workflow-service.js +164 -0
- package/dist/services/workflow-service.js.map +1 -0
- package/dist/sse/events.d.ts +50 -0
- package/dist/sse/events.d.ts.map +1 -0
- package/dist/sse/events.js +152 -0
- package/dist/sse/events.js.map +1 -0
- package/dist/sse/index.d.ts +4 -0
- package/dist/sse/index.d.ts.map +1 -0
- package/dist/sse/index.js +7 -0
- package/dist/sse/index.js.map +1 -0
- package/dist/sse/stream.d.ts +109 -0
- package/dist/sse/stream.d.ts.map +1 -0
- package/dist/sse/stream.js +239 -0
- package/dist/sse/stream.js.map +1 -0
- package/dist/sse/subscriptions.d.ts +91 -0
- package/dist/sse/subscriptions.d.ts.map +1 -0
- package/dist/sse/subscriptions.js +299 -0
- package/dist/sse/subscriptions.js.map +1 -0
- package/dist/types/api.d.ts +651 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +169 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +27 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/monitor.d.ts +243 -0
- package/dist/types/monitor.d.ts.map +1 -0
- package/dist/types/monitor.js +2 -0
- package/dist/types/monitor.js.map +1 -0
- package/dist/types/problem-details.d.ts +63 -0
- package/dist/types/problem-details.d.ts.map +1 -0
- package/dist/types/problem-details.js +37 -0
- package/dist/types/problem-details.js.map +1 -0
- package/dist/types/sse.d.ts +224 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/sse.js +103 -0
- package/dist/types/sse.js.map +1 -0
- package/dist/types/webhook.d.ts +59 -0
- package/dist/types/webhook.d.ts.map +1 -0
- package/dist/types/webhook.js +2 -0
- package/dist/types/webhook.js.map +1 -0
- package/dist/utils/correlation.d.ts +30 -0
- package/dist/utils/correlation.d.ts.map +1 -0
- package/dist/utils/correlation.js +34 -0
- package/dist/utils/correlation.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/shutdown.d.ts +28 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +86 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/dist/worker/claude-cli-worker.d.ts +61 -0
- package/dist/worker/claude-cli-worker.d.ts.map +1 -0
- package/dist/worker/claude-cli-worker.js +330 -0
- package/dist/worker/claude-cli-worker.js.map +1 -0
- package/dist/worker/cli-spawner.d.ts +42 -0
- package/dist/worker/cli-spawner.d.ts.map +1 -0
- package/dist/worker/cli-spawner.js +183 -0
- package/dist/worker/cli-spawner.js.map +1 -0
- package/dist/worker/config.d.ts +76 -0
- package/dist/worker/config.d.ts.map +1 -0
- package/dist/worker/config.js +39 -0
- package/dist/worker/config.js.map +1 -0
- package/dist/worker/epic-post-tasks.d.ts +73 -0
- package/dist/worker/epic-post-tasks.d.ts.map +1 -0
- package/dist/worker/epic-post-tasks.js +266 -0
- package/dist/worker/epic-post-tasks.js.map +1 -0
- package/dist/worker/gate-checker.d.ts +22 -0
- package/dist/worker/gate-checker.d.ts.map +1 -0
- package/dist/worker/gate-checker.js +41 -0
- package/dist/worker/gate-checker.js.map +1 -0
- package/dist/worker/index.d.ts +13 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +19 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/label-manager.d.ts +89 -0
- package/dist/worker/label-manager.d.ts.map +1 -0
- package/dist/worker/label-manager.js +200 -0
- package/dist/worker/label-manager.js.map +1 -0
- package/dist/worker/output-capture.d.ts +58 -0
- package/dist/worker/output-capture.d.ts.map +1 -0
- package/dist/worker/output-capture.js +153 -0
- package/dist/worker/output-capture.js.map +1 -0
- package/dist/worker/phase-loop.d.ts +64 -0
- package/dist/worker/phase-loop.d.ts.map +1 -0
- package/dist/worker/phase-loop.js +185 -0
- package/dist/worker/phase-loop.js.map +1 -0
- package/dist/worker/phase-resolver.d.ts +62 -0
- package/dist/worker/phase-resolver.d.ts.map +1 -0
- package/dist/worker/phase-resolver.js +144 -0
- package/dist/worker/phase-resolver.js.map +1 -0
- package/dist/worker/pr-feedback-handler.d.ts +105 -0
- package/dist/worker/pr-feedback-handler.d.ts.map +1 -0
- package/dist/worker/pr-feedback-handler.js +391 -0
- package/dist/worker/pr-feedback-handler.js.map +1 -0
- package/dist/worker/pr-linker.d.ts +69 -0
- package/dist/worker/pr-linker.d.ts.map +1 -0
- package/dist/worker/pr-linker.js +108 -0
- package/dist/worker/pr-linker.js.map +1 -0
- package/dist/worker/pr-manager.d.ts +61 -0
- package/dist/worker/pr-manager.d.ts.map +1 -0
- package/dist/worker/pr-manager.js +140 -0
- package/dist/worker/pr-manager.js.map +1 -0
- package/dist/worker/repo-checkout.d.ts +63 -0
- package/dist/worker/repo-checkout.d.ts.map +1 -0
- package/dist/worker/repo-checkout.js +160 -0
- package/dist/worker/repo-checkout.js.map +1 -0
- package/dist/worker/stage-comment-manager.d.ts +34 -0
- package/dist/worker/stage-comment-manager.d.ts.map +1 -0
- package/dist/worker/stage-comment-manager.js +114 -0
- package/dist/worker/stage-comment-manager.js.map +1 -0
- package/dist/worker/types.d.ts +192 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +53 -0
- package/dist/worker/types.js.map +1 -0
- 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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"}
|