patchrelay 0.7.9 → 0.8.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/dist/build-info.json +3 -3
- package/dist/cli/commands/feed.js +17 -10
- package/dist/cli/formatters/text.js +16 -3
- package/dist/cli/help.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/operator-client.js +16 -0
- package/dist/config.js +169 -56
- package/dist/db/authoritative-ledger-store.js +6 -2
- package/dist/db/issue-workflow-coordinator.js +11 -0
- package/dist/db/issue-workflow-store.js +1 -0
- package/dist/db/migrations.js +22 -1
- package/dist/db/operator-feed-store.js +21 -3
- package/dist/db/webhook-event-store.js +13 -0
- package/dist/http.js +20 -10
- package/dist/install.js +18 -3
- package/dist/linear-workflow.js +20 -5
- package/dist/operator-feed.js +30 -12
- package/dist/preflight.js +5 -2
- package/dist/reconciliation-snapshot-builder.js +2 -1
- package/dist/service-stage-finalizer.js +243 -2
- package/dist/service-stage-runner.js +60 -42
- package/dist/service-webhook-processor.js +87 -11
- package/dist/service.js +1 -0
- package/dist/stage-failure.js +3 -3
- package/dist/stage-handoff.js +107 -0
- package/dist/stage-launch.js +38 -8
- package/dist/stage-lifecycle-publisher.js +37 -12
- package/dist/webhook-agent-session-handler.js +11 -3
- package/dist/webhook-desired-stage-recorder.js +24 -4
- package/dist/workflow-policy.js +115 -8
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { buildAwaitingHandoffSessionPlan, buildCompletedSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
|
|
1
|
+
import { buildAwaitingHandoffSessionPlan, buildCompletedSessionPlan, buildPreparingSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
|
|
2
2
|
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
3
|
-
import { buildAwaitingHandoffComment, buildRunningStatusComment, resolveActiveLinearState, resolveWorkflowLabelCleanup, resolveWorkflowLabelNames, } from "./linear-workflow.js";
|
|
3
|
+
import { buildAwaitingHandoffComment, buildHumanNeededComment, buildRunningStatusComment, resolveActiveLinearState, resolveWorkflowLabelCleanup, resolveWorkflowLabelNames, } from "./linear-workflow.js";
|
|
4
4
|
import { sanitizeDiagnosticText } from "./utils.js";
|
|
5
5
|
export class StageLifecyclePublisher {
|
|
6
6
|
config;
|
|
@@ -16,7 +16,7 @@ export class StageLifecyclePublisher {
|
|
|
16
16
|
this.feed = feed;
|
|
17
17
|
}
|
|
18
18
|
async markStageActive(project, issue, stageRun) {
|
|
19
|
-
const activeState = resolveActiveLinearState(project, stageRun.stage);
|
|
19
|
+
const activeState = resolveActiveLinearState(project, stageRun.stage, issue.selectedWorkflowId);
|
|
20
20
|
const linear = await this.linearProvider.forProject(stageRun.projectId);
|
|
21
21
|
if (!activeState || !linear) {
|
|
22
22
|
return;
|
|
@@ -84,7 +84,7 @@ export class StageLifecyclePublisher {
|
|
|
84
84
|
await linear.createAgentActivity({
|
|
85
85
|
agentSessionId: issue.activeAgentSessionId,
|
|
86
86
|
content: {
|
|
87
|
-
type: "
|
|
87
|
+
type: "response",
|
|
88
88
|
body: `PatchRelay started the ${stage} workflow and is working in the background.`,
|
|
89
89
|
},
|
|
90
90
|
});
|
|
@@ -103,24 +103,30 @@ export class StageLifecyclePublisher {
|
|
|
103
103
|
async publishStageCompletion(stageRun, enqueueIssue) {
|
|
104
104
|
const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
105
105
|
if (refreshedIssue?.desiredStage) {
|
|
106
|
+
const linear = await this.linearProvider.forProject(stageRun.projectId);
|
|
107
|
+
if (refreshedIssue.activeAgentSessionId && linear) {
|
|
108
|
+
await this.updateAgentSession(linear, refreshedIssue, buildPreparingSessionPlan(refreshedIssue.desiredStage), refreshedIssue.desiredStage);
|
|
109
|
+
}
|
|
106
110
|
this.feed?.publish({
|
|
107
111
|
level: "info",
|
|
108
112
|
kind: "stage",
|
|
109
113
|
issueKey: refreshedIssue.issueKey,
|
|
110
114
|
projectId: refreshedIssue.projectId,
|
|
111
115
|
stage: stageRun.stage,
|
|
116
|
+
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
117
|
+
nextStage: refreshedIssue.desiredStage,
|
|
112
118
|
status: "queued",
|
|
113
119
|
summary: `Completed ${stageRun.stage} workflow and queued ${refreshedIssue.desiredStage}`,
|
|
114
120
|
});
|
|
115
121
|
await this.publishAgentCompletion(refreshedIssue, {
|
|
116
122
|
type: "thought",
|
|
117
|
-
body: `The ${stageRun.stage} workflow finished. PatchRelay is preparing the
|
|
123
|
+
body: `The ${stageRun.stage} workflow finished. PatchRelay is preparing the ${refreshedIssue.desiredStage} workflow next.`,
|
|
118
124
|
});
|
|
119
125
|
enqueueIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
122
128
|
const project = this.config.projects.find((candidate) => candidate.id === stageRun.projectId);
|
|
123
|
-
const activeState = project ? resolveActiveLinearState(project, stageRun.stage) : undefined;
|
|
129
|
+
const activeState = project ? resolveActiveLinearState(project, stageRun.stage, refreshedIssue?.selectedWorkflowId) : undefined;
|
|
124
130
|
const linear = project ? await this.linearProvider.forProject(stageRun.projectId) : undefined;
|
|
125
131
|
if (refreshedIssue && linear && project && activeState) {
|
|
126
132
|
try {
|
|
@@ -146,6 +152,7 @@ export class StageLifecyclePublisher {
|
|
|
146
152
|
issueKey: refreshedIssue.issueKey,
|
|
147
153
|
projectId: refreshedIssue.projectId,
|
|
148
154
|
stage: stageRun.stage,
|
|
155
|
+
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
149
156
|
status: "handoff",
|
|
150
157
|
summary: `Completed ${stageRun.stage} workflow`,
|
|
151
158
|
detail: `Waiting for a Linear state change or follow-up input while the issue remains in ${activeState}.`,
|
|
@@ -155,7 +162,7 @@ export class StageLifecyclePublisher {
|
|
|
155
162
|
type: "elicitation",
|
|
156
163
|
body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
|
|
157
164
|
})) || deliveredToSession;
|
|
158
|
-
if (!deliveredToSession) {
|
|
165
|
+
if (!deliveredToSession && !refreshedIssue.activeAgentSessionId) {
|
|
159
166
|
const result = await linear.upsertIssueComment({
|
|
160
167
|
issueId: stageRun.linearIssueId,
|
|
161
168
|
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
@@ -188,8 +195,11 @@ export class StageLifecyclePublisher {
|
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
if (refreshedIssue) {
|
|
198
|
+
let deliveredToSession = false;
|
|
191
199
|
if (refreshedIssue.activeAgentSessionId && linear) {
|
|
192
|
-
await this.updateAgentSession(linear, refreshedIssue,
|
|
200
|
+
deliveredToSession = await this.updateAgentSession(linear, refreshedIssue, refreshedIssue.lifecycleStatus === "paused"
|
|
201
|
+
? buildAwaitingHandoffSessionPlan(stageRun.stage)
|
|
202
|
+
: buildCompletedSessionPlan(stageRun.stage));
|
|
193
203
|
}
|
|
194
204
|
this.feed?.publish({
|
|
195
205
|
level: "info",
|
|
@@ -197,13 +207,28 @@ export class StageLifecyclePublisher {
|
|
|
197
207
|
issueKey: refreshedIssue.issueKey,
|
|
198
208
|
projectId: refreshedIssue.projectId,
|
|
199
209
|
stage: stageRun.stage,
|
|
210
|
+
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
200
211
|
status: "completed",
|
|
201
212
|
summary: `Completed ${stageRun.stage} workflow`,
|
|
202
213
|
});
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
deliveredToSession =
|
|
215
|
+
(await this.publishAgentCompletion(refreshedIssue, {
|
|
216
|
+
type: refreshedIssue.lifecycleStatus === "paused" ? "elicitation" : "response",
|
|
217
|
+
body: refreshedIssue.lifecycleStatus === "paused"
|
|
218
|
+
? `PatchRelay finished the ${stageRun.stage} workflow and now needs human input before it can continue.`
|
|
219
|
+
: `PatchRelay finished the ${stageRun.stage} workflow.`,
|
|
220
|
+
})) || deliveredToSession;
|
|
221
|
+
if (refreshedIssue.lifecycleStatus === "paused" && linear && !deliveredToSession) {
|
|
222
|
+
const result = await linear.upsertIssueComment({
|
|
223
|
+
issueId: stageRun.linearIssueId,
|
|
224
|
+
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
225
|
+
body: buildHumanNeededComment({
|
|
226
|
+
issue: refreshedIssue,
|
|
227
|
+
stageRun: this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
231
|
+
}
|
|
207
232
|
}
|
|
208
233
|
}
|
|
209
234
|
async updateAgentSession(linear, issue, plan, stage) {
|
|
@@ -35,7 +35,15 @@ export class AgentSessionWebhookHandler {
|
|
|
35
35
|
const issueControl = normalized.issue ? this.stores.issueControl.getIssueControl(project.id, normalized.issue.id) : undefined;
|
|
36
36
|
const activeRunLease = issueControl?.activeRunLeaseId !== undefined ? this.stores.runLeases.getRunLease(issueControl.activeRunLeaseId) : undefined;
|
|
37
37
|
const activeStage = activeRunLease?.stage;
|
|
38
|
-
const runnableWorkflow = normalized.issue?.stateName
|
|
38
|
+
const runnableWorkflow = normalized.issue?.stateName
|
|
39
|
+
? resolveWorkflowStage(project, normalized.issue.stateName, {
|
|
40
|
+
...(issue?.selectedWorkflowId
|
|
41
|
+
? { workflowDefinitionId: issue.selectedWorkflowId }
|
|
42
|
+
: normalized.issue
|
|
43
|
+
? { issue: normalized.issue }
|
|
44
|
+
: {}),
|
|
45
|
+
})
|
|
46
|
+
: undefined;
|
|
39
47
|
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
40
48
|
if (!delegatedToPatchRelay) {
|
|
41
49
|
if (activeStage) {
|
|
@@ -66,7 +74,7 @@ export class AgentSessionWebhookHandler {
|
|
|
66
74
|
if (desiredStage) {
|
|
67
75
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
68
76
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
69
|
-
type: "
|
|
77
|
+
type: "response",
|
|
70
78
|
body: `PatchRelay started working on the ${desiredStage} workflow and is preparing the workspace.`,
|
|
71
79
|
}, { ephemeral: false });
|
|
72
80
|
return;
|
|
@@ -137,7 +145,7 @@ export class AgentSessionWebhookHandler {
|
|
|
137
145
|
if (!activeRunLease && desiredStage) {
|
|
138
146
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
139
147
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
140
|
-
type: "
|
|
148
|
+
type: "response",
|
|
141
149
|
body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
|
|
142
150
|
}, { ephemeral: false });
|
|
143
151
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { triggerEventAllowed } from "./project-resolution.js";
|
|
2
|
-
import { resolveWorkflowStage } from "./workflow-policy.js";
|
|
2
|
+
import { resolveWorkflowStage, selectWorkflowDefinition } from "./workflow-policy.js";
|
|
3
3
|
function trimPrompt(value) {
|
|
4
4
|
const trimmed = value?.trim();
|
|
5
5
|
return trimmed ? trimmed : undefined;
|
|
@@ -25,7 +25,8 @@ export class WebhookDesiredStageRecorder {
|
|
|
25
25
|
const activeStageRun = issueControl?.activeRunLeaseId !== undefined ? this.stores.issueWorkflows.getStageRun(issueControl.activeRunLeaseId) : undefined;
|
|
26
26
|
const delegatedToPatchRelay = this.isDelegatedToPatchRelay(project, normalized);
|
|
27
27
|
const stageAllowed = triggerEventAllowed(project, normalized.triggerEvent);
|
|
28
|
-
const
|
|
28
|
+
const selectedWorkflowId = this.resolveSelectedWorkflowId(project, normalized, issue, activeStageRun, delegatedToPatchRelay);
|
|
29
|
+
const desiredStage = this.resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay, selectedWorkflowId);
|
|
29
30
|
const launchInput = this.resolveLaunchInput(normalized.agentSession);
|
|
30
31
|
const activeAgentSessionId = normalized.agentSession?.id ??
|
|
31
32
|
(!activeStageRun && (desiredStage || (normalized.triggerEvent === "delegateChanged" && !delegatedToPatchRelay)) ? null : undefined);
|
|
@@ -36,6 +37,7 @@ export class WebhookDesiredStageRecorder {
|
|
|
36
37
|
...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
|
|
37
38
|
...(normalizedIssue.url ? { issueUrl: normalizedIssue.url } : {}),
|
|
38
39
|
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
40
|
+
...(selectedWorkflowId !== undefined ? { selectedWorkflowId } : {}),
|
|
39
41
|
...(desiredStage ? { desiredStage } : {}),
|
|
40
42
|
...(options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
41
43
|
...(activeAgentSessionId !== undefined ? { activeAgentSessionId } : {}),
|
|
@@ -71,7 +73,7 @@ export class WebhookDesiredStageRecorder {
|
|
|
71
73
|
}
|
|
72
74
|
return normalizedIssue.delegateId === installation.actorId;
|
|
73
75
|
}
|
|
74
|
-
resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay) {
|
|
76
|
+
resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay, selectedWorkflowId) {
|
|
75
77
|
const normalizedIssue = normalized.issue;
|
|
76
78
|
if (!normalizedIssue) {
|
|
77
79
|
return undefined;
|
|
@@ -82,7 +84,9 @@ export class WebhookDesiredStageRecorder {
|
|
|
82
84
|
if (!delegatedToPatchRelay || !triggerEventAllowed(project, normalized.triggerEvent)) {
|
|
83
85
|
return undefined;
|
|
84
86
|
}
|
|
85
|
-
const desiredStage = resolveWorkflowStage(project, normalizedIssue.stateName
|
|
87
|
+
const desiredStage = resolveWorkflowStage(project, normalizedIssue.stateName, {
|
|
88
|
+
...(selectedWorkflowId ? { workflowDefinitionId: selectedWorkflowId } : {}),
|
|
89
|
+
});
|
|
86
90
|
if (!desiredStage) {
|
|
87
91
|
return undefined;
|
|
88
92
|
}
|
|
@@ -94,6 +98,22 @@ export class WebhookDesiredStageRecorder {
|
|
|
94
98
|
}
|
|
95
99
|
return desiredStage;
|
|
96
100
|
}
|
|
101
|
+
resolveSelectedWorkflowId(project, normalized, issue, activeStageRun, delegatedToPatchRelay) {
|
|
102
|
+
if (activeStageRun) {
|
|
103
|
+
return issue?.selectedWorkflowId;
|
|
104
|
+
}
|
|
105
|
+
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
|
|
106
|
+
return issue?.selectedWorkflowId;
|
|
107
|
+
}
|
|
108
|
+
if (!delegatedToPatchRelay || !triggerEventAllowed(project, normalized.triggerEvent) || !normalized.issue) {
|
|
109
|
+
return issue?.selectedWorkflowId;
|
|
110
|
+
}
|
|
111
|
+
const selectedWorkflow = selectWorkflowDefinition(project, normalized.issue);
|
|
112
|
+
if (selectedWorkflow) {
|
|
113
|
+
return selectedWorkflow.id;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
97
117
|
resolveLaunchInput(agentSession) {
|
|
98
118
|
const promptBody = trimPrompt(agentSession?.promptBody);
|
|
99
119
|
if (promptBody) {
|
package/dist/workflow-policy.js
CHANGED
|
@@ -2,6 +2,9 @@ function normalize(value) {
|
|
|
2
2
|
const trimmed = value?.trim();
|
|
3
3
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
4
4
|
}
|
|
5
|
+
function normalizeWorkflowLabel(value) {
|
|
6
|
+
return normalize(value)?.replace(/[\s_-]+/g, "");
|
|
7
|
+
}
|
|
5
8
|
function extractIssuePrefix(identifier) {
|
|
6
9
|
const value = identifier?.trim();
|
|
7
10
|
if (!value) {
|
|
@@ -10,25 +13,129 @@ function extractIssuePrefix(identifier) {
|
|
|
10
13
|
const [prefix] = value.split("-", 1);
|
|
11
14
|
return prefix ? prefix.toUpperCase() : undefined;
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
function withWorkflowDefinitionId(workflowDefinitionId) {
|
|
17
|
+
return workflowDefinitionId ? { workflowDefinitionId } : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function listProjectWorkflowDefinitions(project) {
|
|
20
|
+
if (project.workflowDefinitions && project.workflowDefinitions.length > 0) {
|
|
21
|
+
return project.workflowDefinitions;
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: project.workflowSelection?.defaultWorkflowId ?? "default",
|
|
26
|
+
stages: project.workflows,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
export function resolveWorkflowDefinitionById(project, workflowDefinitionId) {
|
|
31
|
+
const normalized = normalize(workflowDefinitionId);
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return listProjectWorkflowDefinitions(project).find((definition) => normalize(definition.id) === normalized);
|
|
36
|
+
}
|
|
37
|
+
export function selectWorkflowDefinition(project, issue) {
|
|
38
|
+
const workflowDefinitions = listProjectWorkflowDefinitions(project);
|
|
39
|
+
if (workflowDefinitions.length === 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const labelNames = new Set((issue?.labelNames ?? []).map((label) => label.trim().toLowerCase()).filter(Boolean));
|
|
43
|
+
const matchedWorkflowIds = new Set();
|
|
44
|
+
for (const rule of project.workflowSelection?.byLabel ?? []) {
|
|
45
|
+
if (labelNames.has(rule.label.trim().toLowerCase())) {
|
|
46
|
+
matchedWorkflowIds.add(rule.workflowId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (matchedWorkflowIds.size === 1) {
|
|
50
|
+
const [workflowId] = [...matchedWorkflowIds];
|
|
51
|
+
return resolveWorkflowDefinitionById(project, workflowId);
|
|
52
|
+
}
|
|
53
|
+
if (matchedWorkflowIds.size > 1) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (project.workflowSelection?.defaultWorkflowId) {
|
|
57
|
+
return resolveWorkflowDefinitionById(project, project.workflowSelection.defaultWorkflowId);
|
|
58
|
+
}
|
|
59
|
+
return workflowDefinitions[0];
|
|
60
|
+
}
|
|
61
|
+
function resolveStageList(project, options) {
|
|
62
|
+
if (options?.workflowDefinitionId) {
|
|
63
|
+
return resolveWorkflowDefinitionById(project, options.workflowDefinitionId)?.stages ?? [];
|
|
64
|
+
}
|
|
65
|
+
if (options?.issue) {
|
|
66
|
+
return selectWorkflowDefinition(project, options.issue)?.stages ?? [];
|
|
67
|
+
}
|
|
68
|
+
return project.workflows;
|
|
69
|
+
}
|
|
70
|
+
export function resolveWorkflow(project, stateName, options) {
|
|
14
71
|
const normalized = normalize(stateName);
|
|
15
72
|
if (!normalized) {
|
|
16
73
|
return undefined;
|
|
17
74
|
}
|
|
18
|
-
return project.
|
|
75
|
+
return resolveStageList(project, options).find((workflow) => normalize(workflow.whenState) === normalized);
|
|
19
76
|
}
|
|
20
|
-
export function resolveWorkflowStage(project, stateName) {
|
|
21
|
-
return resolveWorkflow(project, stateName)?.id;
|
|
77
|
+
export function resolveWorkflowStage(project, stateName, options) {
|
|
78
|
+
return resolveWorkflow(project, stateName, options)?.id;
|
|
22
79
|
}
|
|
23
|
-
export function
|
|
80
|
+
export function resolveWorkflowStageConfig(project, workflowId, workflowDefinitionId) {
|
|
24
81
|
const normalized = normalize(workflowId);
|
|
25
82
|
if (!normalized) {
|
|
26
83
|
return undefined;
|
|
27
84
|
}
|
|
28
|
-
return project.
|
|
85
|
+
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).find((workflow) => normalize(workflow.id) === normalized);
|
|
86
|
+
}
|
|
87
|
+
export function listRunnableStates(project, options) {
|
|
88
|
+
return [...new Set(resolveStageList(project, options).map((workflow) => workflow.whenState))];
|
|
89
|
+
}
|
|
90
|
+
export function listWorkflowStageIds(project, workflowDefinitionId) {
|
|
91
|
+
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).map((workflow) => workflow.id);
|
|
92
|
+
}
|
|
93
|
+
export function resolveWorkflowIndex(project, workflowId, workflowDefinitionId) {
|
|
94
|
+
if (!workflowId) {
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).findIndex((workflow) => workflow.id === workflowId);
|
|
29
98
|
}
|
|
30
|
-
export function
|
|
31
|
-
|
|
99
|
+
export function resolveDefaultTransitionTarget(project, currentStage, workflowDefinitionId) {
|
|
100
|
+
const stages = resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId));
|
|
101
|
+
const currentIndex = resolveWorkflowIndex(project, currentStage, workflowDefinitionId);
|
|
102
|
+
if (currentIndex < 0) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const nextStage = stages[currentIndex + 1]?.id;
|
|
106
|
+
return nextStage ?? "done";
|
|
107
|
+
}
|
|
108
|
+
export function listAllowedTransitionTargets(project, currentStage, workflowDefinitionId) {
|
|
109
|
+
const stages = resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId));
|
|
110
|
+
const currentIndex = resolveWorkflowIndex(project, currentStage, workflowDefinitionId);
|
|
111
|
+
if (currentIndex < 0) {
|
|
112
|
+
return ["human_needed"];
|
|
113
|
+
}
|
|
114
|
+
const targets = new Set(["human_needed"]);
|
|
115
|
+
const defaultTarget = resolveDefaultTransitionTarget(project, currentStage, workflowDefinitionId);
|
|
116
|
+
if (defaultTarget) {
|
|
117
|
+
targets.add(defaultTarget);
|
|
118
|
+
}
|
|
119
|
+
if (currentIndex > 0) {
|
|
120
|
+
targets.add(stages[currentIndex - 1].id);
|
|
121
|
+
}
|
|
122
|
+
if (currentIndex > 1) {
|
|
123
|
+
targets.add(stages[0].id);
|
|
124
|
+
}
|
|
125
|
+
return [...targets];
|
|
126
|
+
}
|
|
127
|
+
export function transitionTargetAllowed(project, currentStage, nextTarget, workflowDefinitionId) {
|
|
128
|
+
return listAllowedTransitionTargets(project, currentStage, workflowDefinitionId).includes(nextTarget);
|
|
129
|
+
}
|
|
130
|
+
export function resolveWorkflowStageCandidate(project, value, workflowDefinitionId) {
|
|
131
|
+
const normalized = normalizeWorkflowLabel(value);
|
|
132
|
+
if (!normalized) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).find((workflow) => {
|
|
136
|
+
const candidates = [workflow.id, workflow.whenState, workflow.activeState];
|
|
137
|
+
return candidates.some((candidate) => normalizeWorkflowLabel(candidate) === normalized);
|
|
138
|
+
})?.id;
|
|
32
139
|
}
|
|
33
140
|
export function matchesProject(issue, project) {
|
|
34
141
|
const issuePrefix = extractIssuePrefix(issue.identifier);
|