patchrelay 0.8.8 → 0.9.0
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/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +12 -12
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +21 -54
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /package/dist/{installation-ports.js → github-types.js} +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { buildPreparingSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
|
|
2
|
+
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
3
|
+
import { resolveProject, triggerEventAllowed, trustedActorAllowed } from "./project-resolution.js";
|
|
4
|
+
import { normalizeWebhook } from "./webhooks.js";
|
|
5
|
+
import { InstallationWebhookHandler } from "./webhook-installation-handler.js";
|
|
6
|
+
import { safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
7
|
+
export class WebhookHandler {
|
|
8
|
+
config;
|
|
9
|
+
db;
|
|
10
|
+
linearProvider;
|
|
11
|
+
codex;
|
|
12
|
+
enqueueIssue;
|
|
13
|
+
logger;
|
|
14
|
+
feed;
|
|
15
|
+
installationHandler;
|
|
16
|
+
constructor(config, db, linearProvider, codex, enqueueIssue, logger, feed) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.db = db;
|
|
19
|
+
this.linearProvider = linearProvider;
|
|
20
|
+
this.codex = codex;
|
|
21
|
+
this.enqueueIssue = enqueueIssue;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.feed = feed;
|
|
24
|
+
this.installationHandler = new InstallationWebhookHandler(config, { linearInstallations: db.linearInstallations }, logger);
|
|
25
|
+
}
|
|
26
|
+
async processWebhookEvent(webhookEventId) {
|
|
27
|
+
const event = this.db.getWebhookPayload(webhookEventId);
|
|
28
|
+
if (!event) {
|
|
29
|
+
this.logger.warn({ webhookEventId }, "Webhook event was not found during processing");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const payload = safeJsonParse(event.payloadJson);
|
|
34
|
+
if (!payload) {
|
|
35
|
+
this.db.markWebhookProcessed(webhookEventId, "failed");
|
|
36
|
+
throw new Error(`Stored webhook payload is invalid JSON: event ${webhookEventId}`);
|
|
37
|
+
}
|
|
38
|
+
let normalized = normalizeWebhook({ webhookId: event.webhookId, payload });
|
|
39
|
+
this.logger.info({
|
|
40
|
+
webhookEventId,
|
|
41
|
+
webhookId: event.webhookId,
|
|
42
|
+
triggerEvent: normalized.triggerEvent,
|
|
43
|
+
issueKey: normalized.issue?.identifier,
|
|
44
|
+
}, "Processing stored webhook event");
|
|
45
|
+
if (!normalized.issue) {
|
|
46
|
+
this.feed?.publish({
|
|
47
|
+
level: "info",
|
|
48
|
+
kind: "webhook",
|
|
49
|
+
status: normalized.triggerEvent,
|
|
50
|
+
summary: `Received ${normalized.triggerEvent} webhook`,
|
|
51
|
+
});
|
|
52
|
+
this.installationHandler.handle(normalized);
|
|
53
|
+
this.db.markWebhookProcessed(webhookEventId, "processed");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let project = resolveProject(this.config, normalized.issue);
|
|
57
|
+
if (!project) {
|
|
58
|
+
const routed = await this.tryHydrateProjectRoute(normalized);
|
|
59
|
+
if (routed) {
|
|
60
|
+
normalized = routed.normalized;
|
|
61
|
+
project = routed.project;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!project) {
|
|
65
|
+
this.feed?.publish({
|
|
66
|
+
level: "warn",
|
|
67
|
+
kind: "webhook",
|
|
68
|
+
issueKey: normalized.issue?.identifier,
|
|
69
|
+
status: "ignored",
|
|
70
|
+
summary: "Ignored webhook with no matching project route",
|
|
71
|
+
});
|
|
72
|
+
this.db.markWebhookProcessed(webhookEventId, "processed");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const routedIssue = normalized.issue;
|
|
76
|
+
if (!routedIssue) {
|
|
77
|
+
this.db.markWebhookProcessed(webhookEventId, "failed");
|
|
78
|
+
throw new Error(`Issue context disappeared while routing webhook ${event.webhookId}`);
|
|
79
|
+
}
|
|
80
|
+
if (!trustedActorAllowed(project, normalized.actor)) {
|
|
81
|
+
this.feed?.publish({
|
|
82
|
+
level: "warn",
|
|
83
|
+
kind: "webhook",
|
|
84
|
+
issueKey: routedIssue.identifier,
|
|
85
|
+
projectId: project.id,
|
|
86
|
+
status: "ignored",
|
|
87
|
+
summary: "Ignored webhook from an untrusted actor",
|
|
88
|
+
});
|
|
89
|
+
this.db.markWebhookProcessed(webhookEventId, "processed");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.db.assignWebhookProject(webhookEventId, project.id);
|
|
93
|
+
const hydrated = await this.hydrateIssueContext(project.id, normalized);
|
|
94
|
+
const issue = hydrated.issue ?? routedIssue;
|
|
95
|
+
// Record desired stage and upsert issue
|
|
96
|
+
const result = this.recordDesiredStage(project, hydrated);
|
|
97
|
+
const trackedIssue = result.issue;
|
|
98
|
+
// Handle agent session events
|
|
99
|
+
await this.handleAgentSession(hydrated, project, trackedIssue, result.desiredStage, result.delegated);
|
|
100
|
+
// Handle comments during active run
|
|
101
|
+
await this.handleComment(hydrated, project, trackedIssue);
|
|
102
|
+
this.db.markWebhookProcessed(webhookEventId, "processed");
|
|
103
|
+
if (result.desiredStage) {
|
|
104
|
+
this.feed?.publish({
|
|
105
|
+
level: "info",
|
|
106
|
+
kind: "stage",
|
|
107
|
+
issueKey: issue.identifier,
|
|
108
|
+
projectId: project.id,
|
|
109
|
+
stage: result.desiredStage,
|
|
110
|
+
status: "queued",
|
|
111
|
+
summary: `Queued ${result.desiredStage} workflow`,
|
|
112
|
+
detail: `Triggered by ${hydrated.triggerEvent}.`,
|
|
113
|
+
});
|
|
114
|
+
this.enqueueIssue(project.id, issue.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
this.db.markWebhookProcessed(webhookEventId, "failed");
|
|
119
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
120
|
+
this.feed?.publish({
|
|
121
|
+
level: "error",
|
|
122
|
+
kind: "webhook",
|
|
123
|
+
projectId: undefined,
|
|
124
|
+
status: "failed",
|
|
125
|
+
summary: "Failed to process webhook",
|
|
126
|
+
detail: sanitizeDiagnosticText(err.message),
|
|
127
|
+
});
|
|
128
|
+
this.logger.error({ webhookEventId, webhookId: event.webhookId, error: sanitizeDiagnosticText(err.message) }, "Failed to process Linear webhook event");
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
recordDesiredStage(project, normalized) {
|
|
133
|
+
const normalizedIssue = normalized.issue;
|
|
134
|
+
if (!normalizedIssue) {
|
|
135
|
+
return { issue: undefined, desiredStage: undefined, delegated: false };
|
|
136
|
+
}
|
|
137
|
+
const existingIssue = this.db.getIssue(project.id, normalizedIssue.id);
|
|
138
|
+
const activeRun = existingIssue?.activeRunId ? this.db.getRun(existingIssue.activeRunId) : undefined;
|
|
139
|
+
const delegated = this.isDelegatedToPatchRelay(project, normalized);
|
|
140
|
+
const triggerAllowed = triggerEventAllowed(project, normalized.triggerEvent);
|
|
141
|
+
// In the factory model, delegation → queue an implementation run.
|
|
142
|
+
// agentSessionCreated is itself a delegation signal (session exists because issue was delegated).
|
|
143
|
+
let pendingRunType;
|
|
144
|
+
const isDelegationSignal = delegated || normalized.triggerEvent === "agentSessionCreated";
|
|
145
|
+
if (isDelegationSignal && triggerAllowed && !activeRun && !existingIssue?.pendingRunType) {
|
|
146
|
+
pendingRunType = "implementation";
|
|
147
|
+
}
|
|
148
|
+
// Resolve agent session
|
|
149
|
+
const agentSessionId = normalized.agentSession?.id ??
|
|
150
|
+
(!activeRun && (pendingRunType || (normalized.triggerEvent === "delegateChanged" && !delegated)) ? null : undefined);
|
|
151
|
+
// Upsert the issue
|
|
152
|
+
const issue = this.db.upsertIssue({
|
|
153
|
+
projectId: project.id,
|
|
154
|
+
linearIssueId: normalizedIssue.id,
|
|
155
|
+
...(normalizedIssue.identifier ? { issueKey: normalizedIssue.identifier } : {}),
|
|
156
|
+
...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
|
|
157
|
+
...(normalizedIssue.url ? { url: normalizedIssue.url } : {}),
|
|
158
|
+
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
159
|
+
...(pendingRunType ? { pendingRunType, factoryState: "delegated" } : {}),
|
|
160
|
+
...(agentSessionId !== undefined ? { agentSessionId } : {}),
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
issue: this.db.issueToTrackedIssue(issue),
|
|
164
|
+
desiredStage: pendingRunType,
|
|
165
|
+
delegated,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
isDelegatedToPatchRelay(project, normalized) {
|
|
169
|
+
if (!normalized.issue)
|
|
170
|
+
return false;
|
|
171
|
+
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
172
|
+
if (!installation?.actorId)
|
|
173
|
+
return false;
|
|
174
|
+
return normalized.issue.delegateId === installation.actorId;
|
|
175
|
+
}
|
|
176
|
+
// ─── Agent session handling (inlined) ─────────────────────────────
|
|
177
|
+
async handleAgentSession(normalized, project, trackedIssue, desiredStage, delegated) {
|
|
178
|
+
if (!normalized.agentSession?.id || !normalized.issue)
|
|
179
|
+
return;
|
|
180
|
+
const linear = await this.linearProvider.forProject(project.id);
|
|
181
|
+
if (!linear)
|
|
182
|
+
return;
|
|
183
|
+
const existingIssue = this.db.getIssue(project.id, normalized.issue.id);
|
|
184
|
+
const activeRun = existingIssue?.activeRunId ? this.db.getRun(existingIssue.activeRunId) : undefined;
|
|
185
|
+
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
186
|
+
if (!delegated) {
|
|
187
|
+
const body = "PatchRelay received your mention. Delegate the issue to PatchRelay to start work.";
|
|
188
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, { type: "elicitation", body });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (desiredStage) {
|
|
192
|
+
await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildPreparingSessionPlan(desiredStage));
|
|
193
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
194
|
+
type: "response",
|
|
195
|
+
body: `PatchRelay started working on the ${desiredStage} workflow.`,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (activeRun) {
|
|
200
|
+
await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildRunningSessionPlan(activeRun.runType));
|
|
201
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
202
|
+
type: "response",
|
|
203
|
+
body: `PatchRelay is already running the ${activeRun.runType} workflow for this issue.`,
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
208
|
+
type: "elicitation",
|
|
209
|
+
body: "PatchRelay is delegated, but no work is queued. Delegate the issue or move it to Start to trigger implementation.",
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (normalized.triggerEvent !== "agentPrompted")
|
|
214
|
+
return;
|
|
215
|
+
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
216
|
+
return;
|
|
217
|
+
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
218
|
+
if (activeRun && promptBody && activeRun.threadId && activeRun.turnId) {
|
|
219
|
+
// Deliver prompt directly to active Codex turn
|
|
220
|
+
const input = `New Linear agent prompt received while you are working.\n\n${promptBody}`;
|
|
221
|
+
try {
|
|
222
|
+
await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
|
|
223
|
+
this.feed?.publish({
|
|
224
|
+
level: "info",
|
|
225
|
+
kind: "agent",
|
|
226
|
+
projectId: project.id,
|
|
227
|
+
issueKey: trackedIssue?.issueKey,
|
|
228
|
+
stage: activeRun.runType,
|
|
229
|
+
status: "delivered",
|
|
230
|
+
summary: `Delivered follow-up prompt to active ${activeRun.runType} workflow`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
this.feed?.publish({
|
|
235
|
+
level: "warn",
|
|
236
|
+
kind: "agent",
|
|
237
|
+
projectId: project.id,
|
|
238
|
+
issueKey: trackedIssue?.issueKey,
|
|
239
|
+
stage: activeRun.runType,
|
|
240
|
+
status: "delivery_failed",
|
|
241
|
+
summary: `Could not deliver follow-up prompt to active ${activeRun.runType} workflow`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
245
|
+
type: "thought",
|
|
246
|
+
body: `PatchRelay routed your follow-up instructions into the active ${activeRun.runType} workflow.`,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (desiredStage) {
|
|
251
|
+
await this.updateAgentSessionPlan(linear, project, normalized.agentSession.id, trackedIssue, buildPreparingSessionPlan(desiredStage));
|
|
252
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
253
|
+
type: "response",
|
|
254
|
+
body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ─── Comment handling (inlined) ───────────────────────────────────
|
|
259
|
+
async handleComment(normalized, project, trackedIssue) {
|
|
260
|
+
if ((normalized.triggerEvent !== "commentCreated" && normalized.triggerEvent !== "commentUpdated") ||
|
|
261
|
+
!normalized.comment?.body ||
|
|
262
|
+
!normalized.issue) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
266
|
+
return;
|
|
267
|
+
const issue = this.db.getIssue(project.id, normalized.issue.id);
|
|
268
|
+
if (!issue?.activeRunId)
|
|
269
|
+
return;
|
|
270
|
+
const run = this.db.getRun(issue.activeRunId);
|
|
271
|
+
if (!run?.threadId || !run.turnId)
|
|
272
|
+
return;
|
|
273
|
+
const body = [
|
|
274
|
+
"New Linear comment received while you are working.",
|
|
275
|
+
normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
|
|
276
|
+
"",
|
|
277
|
+
normalized.comment.body.trim(),
|
|
278
|
+
].filter(Boolean).join("\n");
|
|
279
|
+
try {
|
|
280
|
+
await this.codex.steerTurn({ threadId: run.threadId, turnId: run.turnId, input: body });
|
|
281
|
+
this.feed?.publish({
|
|
282
|
+
level: "info",
|
|
283
|
+
kind: "comment",
|
|
284
|
+
projectId: project.id,
|
|
285
|
+
issueKey: trackedIssue?.issueKey,
|
|
286
|
+
stage: run.runType,
|
|
287
|
+
status: "delivered",
|
|
288
|
+
summary: `Delivered follow-up comment to active ${run.runType} workflow`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
this.feed?.publish({
|
|
293
|
+
level: "warn",
|
|
294
|
+
kind: "comment",
|
|
295
|
+
projectId: project.id,
|
|
296
|
+
issueKey: trackedIssue?.issueKey,
|
|
297
|
+
stage: run.runType,
|
|
298
|
+
status: "delivery_failed",
|
|
299
|
+
summary: `Could not deliver follow-up comment to active ${run.runType} workflow`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
304
|
+
async publishAgentActivity(linear, agentSessionId, content) {
|
|
305
|
+
try {
|
|
306
|
+
await linear.createAgentActivity({
|
|
307
|
+
agentSessionId,
|
|
308
|
+
content,
|
|
309
|
+
ephemeral: content.type === "thought",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
this.logger.warn({ agentSessionId, error: error instanceof Error ? error.message : String(error) }, "Failed to publish Linear agent activity");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async updateAgentSessionPlan(linear, project, agentSessionId, issue, plan) {
|
|
317
|
+
if (!linear.updateAgentSession)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
const externalUrls = buildAgentSessionExternalUrls(this.config, issue?.issueKey);
|
|
321
|
+
await linear.updateAgentSession({
|
|
322
|
+
agentSessionId,
|
|
323
|
+
...(externalUrls ? { externalUrls } : {}),
|
|
324
|
+
plan,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
this.logger.warn({ agentSessionId, error: error instanceof Error ? error.message : String(error) }, "Failed to update Linear agent session");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async hydrateIssueContext(projectId, normalized) {
|
|
332
|
+
if (!normalized.issue)
|
|
333
|
+
return normalized;
|
|
334
|
+
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted")
|
|
335
|
+
return normalized;
|
|
336
|
+
if (hasCompleteIssueContext(normalized.issue))
|
|
337
|
+
return normalized;
|
|
338
|
+
const linear = await this.linearProvider.forProject(projectId);
|
|
339
|
+
if (!linear)
|
|
340
|
+
return normalized;
|
|
341
|
+
try {
|
|
342
|
+
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
343
|
+
return { ...normalized, issue: mergeIssueMetadata(normalized.issue, liveIssue) };
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return normalized;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async tryHydrateProjectRoute(normalized) {
|
|
350
|
+
if (!normalized.issue)
|
|
351
|
+
return undefined;
|
|
352
|
+
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted")
|
|
353
|
+
return undefined;
|
|
354
|
+
for (const candidate of this.config.projects) {
|
|
355
|
+
const linear = await this.linearProvider.forProject(candidate.id);
|
|
356
|
+
if (!linear)
|
|
357
|
+
continue;
|
|
358
|
+
try {
|
|
359
|
+
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
360
|
+
const hydrated = { ...normalized, issue: mergeIssueMetadata(normalized.issue, liveIssue) };
|
|
361
|
+
const resolved = resolveProject(this.config, hydrated.issue);
|
|
362
|
+
if (resolved)
|
|
363
|
+
return { project: resolved, normalized: hydrated };
|
|
364
|
+
}
|
|
365
|
+
catch { /* continue to next candidate */ }
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function hasCompleteIssueContext(issue) {
|
|
371
|
+
return Boolean(issue.stateName && issue.delegateId && issue.teamId && issue.teamKey);
|
|
372
|
+
}
|
|
373
|
+
function mergeIssueMetadata(issue, liveIssue) {
|
|
374
|
+
return {
|
|
375
|
+
...issue,
|
|
376
|
+
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
377
|
+
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
378
|
+
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
379
|
+
...(issue.teamId ? {} : liveIssue.teamId ? { teamId: liveIssue.teamId } : {}),
|
|
380
|
+
...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
|
|
381
|
+
...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
|
|
382
|
+
...(issue.stateName ? {} : liveIssue.stateName ? { stateName: liveIssue.stateName } : {}),
|
|
383
|
+
...(issue.delegateId ? {} : liveIssue.delegateId ? { delegateId: liveIssue.delegateId } : {}),
|
|
384
|
+
...(issue.delegateName ? {} : liveIssue.delegateName ? { delegateName: liveIssue.delegateName } : {}),
|
|
385
|
+
labelNames: issue.labelNames.length > 0 ? issue.labelNames : (liveIssue.labels ?? []).map((l) => l.name),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
@@ -8,9 +8,8 @@ export class InstallationWebhookHandler {
|
|
|
8
8
|
this.logger = logger;
|
|
9
9
|
}
|
|
10
10
|
handle(normalized) {
|
|
11
|
-
if (!normalized.installation)
|
|
11
|
+
if (!normalized.installation)
|
|
12
12
|
return;
|
|
13
|
-
}
|
|
14
13
|
if (normalized.triggerEvent === "installationPermissionsChanged") {
|
|
15
14
|
const matchingInstallations = normalized.installation.appUserId
|
|
16
15
|
? this.stores.linearInstallations
|
|
@@ -24,11 +23,7 @@ export class InstallationWebhookHandler {
|
|
|
24
23
|
const project = this.config.projects.find((entry) => entry.id === link.projectId);
|
|
25
24
|
const removedMatches = normalized.installation?.removedTeamIds.some((teamId) => project?.linearTeamIds.includes(teamId)) ?? false;
|
|
26
25
|
const addedMatches = normalized.installation?.addedTeamIds.some((teamId) => project?.linearTeamIds.includes(teamId)) ?? false;
|
|
27
|
-
return {
|
|
28
|
-
projectId: link.projectId,
|
|
29
|
-
removedMatches,
|
|
30
|
-
addedMatches,
|
|
31
|
-
};
|
|
26
|
+
return { projectId: link.projectId, removedMatches, addedMatches };
|
|
32
27
|
}));
|
|
33
28
|
this.logger.warn({
|
|
34
29
|
appUserId: normalized.installation.appUserId,
|
|
@@ -43,7 +38,7 @@ export class InstallationWebhookHandler {
|
|
|
43
38
|
this.logger.warn({
|
|
44
39
|
organizationId: normalized.installation.organizationId,
|
|
45
40
|
oauthClientId: normalized.installation.oauthClientId,
|
|
46
|
-
}, "Linear OAuth app installation was revoked; reconnect affected projects
|
|
41
|
+
}, "Linear OAuth app installation was revoked; reconnect affected projects");
|
|
47
42
|
return;
|
|
48
43
|
}
|
|
49
44
|
if (normalized.triggerEvent === "appUserNotification") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchrelay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"start": "node dist/index.js serve",
|
|
34
34
|
"doctor": "node dist/index.js doctor",
|
|
35
35
|
"restart": "node dist/index.js restart-service",
|
|
36
|
+
"deploy": "npm run build && npm install -g . && node dist/index.js restart-service",
|
|
36
37
|
"lint": "eslint .",
|
|
37
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
39
|
"check": "npm run typecheck",
|