patchrelay 0.8.3 → 0.8.5
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/dist/build-info.json
CHANGED
package/dist/linear-workflow.js
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
import { resolveWorkflowStageConfig } from "./workflow-policy.js";
|
|
2
2
|
const STATUS_MARKER = "<!-- patchrelay:status-comment -->";
|
|
3
|
+
function normalizeLinearState(value) {
|
|
4
|
+
const trimmed = value?.trim();
|
|
5
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
6
|
+
}
|
|
3
7
|
export function resolveActiveLinearState(project, stage, workflowDefinitionId) {
|
|
4
8
|
return resolveWorkflowStageConfig(project, stage, workflowDefinitionId)?.activeState;
|
|
5
9
|
}
|
|
6
10
|
export function resolveFallbackLinearState(project, stage, workflowDefinitionId) {
|
|
7
11
|
return resolveWorkflowStageConfig(project, stage, workflowDefinitionId)?.fallbackState;
|
|
8
12
|
}
|
|
13
|
+
export function resolveDoneLinearState(issue) {
|
|
14
|
+
const typedMatch = issue.workflowStates.find((state) => normalizeLinearState(state.type) === "completed");
|
|
15
|
+
if (typedMatch?.name) {
|
|
16
|
+
return typedMatch.name;
|
|
17
|
+
}
|
|
18
|
+
const nameMatch = issue.workflowStates.find((state) => {
|
|
19
|
+
const normalized = normalizeLinearState(state.name);
|
|
20
|
+
return normalized === "done" || normalized === "completed" || normalized === "complete";
|
|
21
|
+
});
|
|
22
|
+
return nameMatch?.name;
|
|
23
|
+
}
|
|
24
|
+
export function resolveAuthoritativeLinearStopState(issue) {
|
|
25
|
+
const currentStateName = issue.stateName?.trim();
|
|
26
|
+
const normalizedCurrentState = normalizeLinearState(currentStateName);
|
|
27
|
+
if (!currentStateName || !normalizedCurrentState) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const currentWorkflowState = issue.workflowStates.find((state) => normalizeLinearState(state.name) === normalizedCurrentState);
|
|
31
|
+
if (normalizeLinearState(currentWorkflowState?.type) === "completed") {
|
|
32
|
+
return {
|
|
33
|
+
stateName: currentWorkflowState?.name ?? currentStateName,
|
|
34
|
+
lifecycleStatus: "completed",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (normalizedCurrentState === "human needed") {
|
|
38
|
+
return {
|
|
39
|
+
stateName: currentStateName,
|
|
40
|
+
lifecycleStatus: "paused",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (normalizedCurrentState === "done" || normalizedCurrentState === "completed" || normalizedCurrentState === "complete") {
|
|
44
|
+
return {
|
|
45
|
+
stateName: currentStateName,
|
|
46
|
+
lifecycleStatus: "completed",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
9
51
|
export function buildRunningStatusComment(params) {
|
|
10
52
|
return [
|
|
11
53
|
STATUS_MARKER,
|
|
@@ -3,7 +3,7 @@ import { reconcileIssue } from "./reconciliation-engine.js";
|
|
|
3
3
|
import { buildReconciliationSnapshot } from "./reconciliation-snapshot-builder.js";
|
|
4
4
|
import { syncFailedStageToLinear } from "./stage-failure.js";
|
|
5
5
|
import { parseStageHandoff } from "./stage-handoff.js";
|
|
6
|
-
import { resolveFallbackLinearState } from "./linear-workflow.js";
|
|
6
|
+
import { resolveAuthoritativeLinearStopState, resolveDoneLinearState, resolveFallbackLinearState } from "./linear-workflow.js";
|
|
7
7
|
import { resolveDefaultTransitionTarget, transitionTargetAllowed } from "./workflow-policy.js";
|
|
8
8
|
import { buildFailedStageReport, buildPendingMaterializationThread, buildStageReport, countEventMethods, extractStageSummary, extractTurnId, resolveStageRunStatus, summarizeCurrentThread, } from "./stage-reporting.js";
|
|
9
9
|
import { StageLifecyclePublisher } from "./stage-lifecycle-publisher.js";
|
|
@@ -241,7 +241,7 @@ export class ServiceStageFinalizer {
|
|
|
241
241
|
}
|
|
242
242
|
async maybeQueueAutomaticTransition(stageRun, report) {
|
|
243
243
|
const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
244
|
-
if (!refreshedIssue
|
|
244
|
+
if (!refreshedIssue) {
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
247
|
const project = this.config.projects.find((candidate) => candidate.id === stageRun.projectId);
|
|
@@ -260,6 +260,14 @@ export class ServiceStageFinalizer {
|
|
|
260
260
|
if (!linearIssue) {
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
|
+
const authoritativeStopState = resolveAuthoritativeLinearStopState(linearIssue);
|
|
264
|
+
if (authoritativeStopState) {
|
|
265
|
+
this.syncIssueToAuthoritativeLinearStopState(stageRun, refreshedIssue, authoritativeStopState);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (refreshedIssue.desiredStage) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
263
271
|
const continuationPrecondition = await this.checkAutomaticContinuationPreconditions(stageRun, refreshedIssue, linear, linearIssue);
|
|
264
272
|
if (!continuationPrecondition.allowed) {
|
|
265
273
|
this.feed?.publish({
|
|
@@ -348,6 +356,32 @@ export class ServiceStageFinalizer {
|
|
|
348
356
|
lifecycleStatus: "queued",
|
|
349
357
|
});
|
|
350
358
|
}
|
|
359
|
+
syncIssueToAuthoritativeLinearStopState(stageRun, issue, stopState) {
|
|
360
|
+
this.stores.workflowCoordinator.setIssueDesiredStage(stageRun.projectId, stageRun.linearIssueId, undefined, {
|
|
361
|
+
lifecycleStatus: stopState.lifecycleStatus,
|
|
362
|
+
});
|
|
363
|
+
this.stores.workflowCoordinator.upsertTrackedIssue({
|
|
364
|
+
projectId: stageRun.projectId,
|
|
365
|
+
linearIssueId: stageRun.linearIssueId,
|
|
366
|
+
currentLinearState: stopState.stateName,
|
|
367
|
+
lifecycleStatus: stopState.lifecycleStatus,
|
|
368
|
+
});
|
|
369
|
+
this.feed?.publish({
|
|
370
|
+
level: "info",
|
|
371
|
+
kind: "workflow",
|
|
372
|
+
issueKey: issue.issueKey,
|
|
373
|
+
projectId: stageRun.projectId,
|
|
374
|
+
stage: stageRun.stage,
|
|
375
|
+
...(issue.selectedWorkflowId ? { workflowId: issue.selectedWorkflowId } : {}),
|
|
376
|
+
status: stopState.lifecycleStatus === "completed" ? "completed" : "transition_suppressed",
|
|
377
|
+
summary: stopState.lifecycleStatus === "completed"
|
|
378
|
+
? `Kept workflow completed after ${stageRun.stage}`
|
|
379
|
+
: `Ignored stale ${stageRun.stage} completion`,
|
|
380
|
+
detail: stopState.lifecycleStatus === "completed"
|
|
381
|
+
? `Live Linear state is already ${stopState.stateName}, so PatchRelay kept the issue finished.`
|
|
382
|
+
: `Live Linear state is already ${stopState.stateName}, so PatchRelay kept the issue paused.`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
351
385
|
async checkAutomaticContinuationPreconditions(stageRun, issue, linear, linearIssue) {
|
|
352
386
|
const actorProfile = await linear.getActorProfile().catch(() => undefined);
|
|
353
387
|
if (actorProfile?.actorId && linearIssue.delegateId && linearIssue.delegateId !== actorProfile.actorId) {
|
|
@@ -651,14 +685,3 @@ function normalizeLinearState(value) {
|
|
|
651
685
|
const trimmed = value?.trim();
|
|
652
686
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
653
687
|
}
|
|
654
|
-
function resolveDoneLinearState(issue) {
|
|
655
|
-
const typedMatch = issue.workflowStates.find((state) => normalizeLinearState(state.type) === "completed");
|
|
656
|
-
if (typedMatch?.name) {
|
|
657
|
-
return typedMatch.name;
|
|
658
|
-
}
|
|
659
|
-
const nameMatch = issue.workflowStates.find((state) => {
|
|
660
|
-
const normalized = normalizeLinearState(state.name);
|
|
661
|
-
return normalized === "done" || normalized === "completed" || normalized === "complete";
|
|
662
|
-
});
|
|
663
|
-
return nameMatch?.name;
|
|
664
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
|
|
1
2
|
import { buildStageLaunchPlan, isCodexThreadId } from "./stage-launch.js";
|
|
2
3
|
import { syncFailedStageToLinear } from "./stage-failure.js";
|
|
3
4
|
import { buildFailedStageReport } from "./stage-reporting.js";
|
|
@@ -47,6 +48,22 @@ export class ServiceStageRunner {
|
|
|
47
48
|
if (!issue) {
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
51
|
+
if (issue.lifecycleStatus === "completed" || issue.lifecycleStatus === "paused") {
|
|
52
|
+
this.feed?.publish({
|
|
53
|
+
level: "info",
|
|
54
|
+
kind: "workflow",
|
|
55
|
+
issueKey: issue.issueKey,
|
|
56
|
+
projectId: item.projectId,
|
|
57
|
+
stage: desiredStage,
|
|
58
|
+
...(issue.selectedWorkflowId ? { workflowId: issue.selectedWorkflowId } : {}),
|
|
59
|
+
status: issue.lifecycleStatus === "completed" ? "completed" : "transition_suppressed",
|
|
60
|
+
summary: issue.lifecycleStatus === "completed"
|
|
61
|
+
? `Skipped ${desiredStage} because the issue is already complete`
|
|
62
|
+
: `Skipped ${desiredStage} because the issue is already paused`,
|
|
63
|
+
detail: issue.currentLinearState ? `Live Linear state is ${issue.currentLinearState}.` : undefined,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
50
67
|
const existingWorkspace = this.stores.workspaceOwnership.getWorkspaceOwnershipForIssue(item.projectId, item.issueId);
|
|
51
68
|
const stageHistory = this.stores.issueWorkflows.listStageRunsForIssue(item.projectId, item.issueId);
|
|
52
69
|
const previousStageRun = stageHistory.at(-1);
|
|
@@ -201,13 +218,28 @@ export class ServiceStageRunner {
|
|
|
201
218
|
}
|
|
202
219
|
async ensureLaunchIssueMirror(project, linearIssueId, _desiredStage, _desiredWebhookId) {
|
|
203
220
|
const existing = this.stores.issueWorkflows.getTrackedIssue(project.id, linearIssueId);
|
|
204
|
-
if (existing?.issueKey && existing.title && existing.issueUrl && existing.currentLinearState) {
|
|
205
|
-
return existing;
|
|
206
|
-
}
|
|
207
221
|
const liveIssue = await this.linearProvider
|
|
208
222
|
.forProject(project.id)
|
|
209
223
|
.then((linear) => linear?.getIssue(linearIssueId))
|
|
210
224
|
.catch(() => undefined);
|
|
225
|
+
const authoritativeStopState = liveIssue ? resolveAuthoritativeLinearStopState(liveIssue) : undefined;
|
|
226
|
+
if (authoritativeStopState) {
|
|
227
|
+
this.stores.workflowCoordinator.setIssueDesiredStage(project.id, linearIssueId, undefined, {
|
|
228
|
+
lifecycleStatus: authoritativeStopState.lifecycleStatus,
|
|
229
|
+
});
|
|
230
|
+
return this.stores.workflowCoordinator.upsertTrackedIssue({
|
|
231
|
+
projectId: project.id,
|
|
232
|
+
linearIssueId,
|
|
233
|
+
...(liveIssue?.identifier ? { issueKey: liveIssue.identifier } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
|
|
234
|
+
...(liveIssue?.title ? { title: liveIssue.title } : existing?.title ? { title: existing.title } : {}),
|
|
235
|
+
...(liveIssue?.url ? { issueUrl: liveIssue.url } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
|
|
236
|
+
currentLinearState: authoritativeStopState.stateName,
|
|
237
|
+
lifecycleStatus: authoritativeStopState.lifecycleStatus,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (!liveIssue && existing?.issueKey && existing.title && existing.issueUrl && existing.currentLinearState) {
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
211
243
|
return this.stores.workflowCoordinator.recordDesiredStage({
|
|
212
244
|
projectId: project.id,
|
|
213
245
|
linearIssueId,
|
|
@@ -39,7 +39,7 @@ export class WebhookDesiredStageRecorder {
|
|
|
39
39
|
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
40
40
|
...(selectedWorkflowId !== undefined ? { selectedWorkflowId } : {}),
|
|
41
41
|
...(desiredStage ? { desiredStage } : {}),
|
|
42
|
-
...(options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
42
|
+
...(desiredStage && options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
43
43
|
...(activeAgentSessionId !== undefined ? { activeAgentSessionId } : {}),
|
|
44
44
|
lastWebhookAt: new Date().toISOString(),
|
|
45
45
|
});
|