@atolis-hq/wake 0.1.10 → 0.1.12
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/src/adapters/fake/fake-ticketing-system.js +9 -1
- package/dist/src/adapters/github/github-issues-work-source.js +11 -1
- package/dist/src/core/policy-engine.js +19 -3
- package/dist/src/core/projection-updater.js +80 -26
- package/dist/src/core/tick-runner.js +17 -6
- package/dist/src/domain/schema.js +30 -0
- package/dist/src/domain/workflows.js +73 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -91,14 +91,22 @@ export function createFakeTicketingSystem(options) {
|
|
|
91
91
|
const stageLabel = typeof input.event.payload.stageLabel === 'string'
|
|
92
92
|
? input.event.payload.stageLabel
|
|
93
93
|
: undefined;
|
|
94
|
+
const workflowLabel = typeof input.event.payload.workflowLabel === 'string'
|
|
95
|
+
? input.event.payload.workflowLabel
|
|
96
|
+
: undefined;
|
|
94
97
|
const labels = [
|
|
95
|
-
...currentLabels.filter((label) => !label.startsWith('wake:status.') &&
|
|
98
|
+
...currentLabels.filter((label) => !label.startsWith('wake:status.') &&
|
|
99
|
+
!label.startsWith('wake:stage.') &&
|
|
100
|
+
!label.startsWith('wake:workflow.')),
|
|
96
101
|
...(statusLabel === undefined
|
|
97
102
|
? currentLabels.filter((label) => label.startsWith('wake:status.'))
|
|
98
103
|
: [statusLabel]),
|
|
99
104
|
...(stageLabel === undefined
|
|
100
105
|
? currentLabels.filter((label) => label.startsWith('wake:stage.'))
|
|
101
106
|
: [stageLabel]),
|
|
107
|
+
...(workflowLabel === undefined
|
|
108
|
+
? currentLabels.filter((label) => label.startsWith('wake:workflow.'))
|
|
109
|
+
: [workflowLabel]),
|
|
102
110
|
];
|
|
103
111
|
return [
|
|
104
112
|
createEventEnvelope({
|
|
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { defaultAgentIdentity } from '../../domain/schema.js';
|
|
4
4
|
import { buildResourceUri } from '../../domain/resource-uri.js';
|
|
5
5
|
import { wakeStageLabelPrefix } from '../../domain/stages.js';
|
|
6
|
+
import { wakeWorkflowLabelPrefix } from '../../domain/workflows.js';
|
|
6
7
|
import { createEventEnvelope, createUnkeyedEventEnvelope } from '../../lib/event-log.js';
|
|
7
8
|
import { wakeVersion } from '../../version.js';
|
|
8
9
|
import { buildResumeCommandForCli } from '../runner/runner-cli-adapter.js';
|
|
@@ -340,14 +341,22 @@ export function createGitHubIssuesWorkSource(deps) {
|
|
|
340
341
|
const nextStageLabel = typeof input.event.payload.stageLabel === 'string'
|
|
341
342
|
? input.event.payload.stageLabel
|
|
342
343
|
: undefined;
|
|
344
|
+
const nextWorkflowLabel = typeof input.event.payload.workflowLabel === 'string'
|
|
345
|
+
? input.event.payload.workflowLabel
|
|
346
|
+
: undefined;
|
|
343
347
|
const nextLabels = [
|
|
344
|
-
...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) &&
|
|
348
|
+
...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) &&
|
|
349
|
+
!label.startsWith(wakeStageLabelPrefix) &&
|
|
350
|
+
!label.startsWith(wakeWorkflowLabelPrefix)),
|
|
345
351
|
...(nextStatusLabel !== undefined
|
|
346
352
|
? [nextStatusLabel]
|
|
347
353
|
: currentLabels.filter((label) => label.startsWith(wakeStatusLabelPrefix))),
|
|
348
354
|
...(nextStageLabel !== undefined
|
|
349
355
|
? [nextStageLabel]
|
|
350
356
|
: currentLabels.filter((label) => label.startsWith(wakeStageLabelPrefix))),
|
|
357
|
+
...(nextWorkflowLabel !== undefined
|
|
358
|
+
? [nextWorkflowLabel]
|
|
359
|
+
: currentLabels.filter((label) => label.startsWith(wakeWorkflowLabelPrefix))),
|
|
351
360
|
];
|
|
352
361
|
const labelsChanged = nextLabels.length !== currentLabels.length ||
|
|
353
362
|
!nextLabels.every((label, index) => label === currentLabels[index]);
|
|
@@ -372,6 +381,7 @@ export function createGitHubIssuesWorkSource(deps) {
|
|
|
372
381
|
intentEventId: input.event.eventId,
|
|
373
382
|
...(nextStatusLabel !== undefined ? { statusLabel: nextStatusLabel } : {}),
|
|
374
383
|
...(nextStageLabel !== undefined ? { stageLabel: nextStageLabel } : {}),
|
|
384
|
+
...(nextWorkflowLabel !== undefined ? { workflowLabel: nextWorkflowLabel } : {}),
|
|
375
385
|
labels: nextLabels,
|
|
376
386
|
providerEventType: 'github.issue.labels.updated',
|
|
377
387
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { awaitingApprovalRunnerSentinel, failedRunnerSentinel } from '../domain/stages.js';
|
|
2
2
|
import { resolveCustomCommand } from '../domain/custom-commands.js';
|
|
3
|
-
import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction, } from '../domain/workflows.js';
|
|
3
|
+
import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction, selectWorkflowForEvent, } from '../domain/workflows.js';
|
|
4
4
|
function isAwaitingApproval(issue) {
|
|
5
5
|
const context = issue.context;
|
|
6
6
|
return context.lastRunSentinel === awaitingApprovalRunnerSentinel;
|
|
@@ -57,6 +57,10 @@ export function createPolicyEngine() {
|
|
|
57
57
|
if (issue.issue.state !== 'open') {
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
|
+
const context = issue.context;
|
|
61
|
+
if (config.workflowSelectors.length > 0) {
|
|
62
|
+
return typeof context.workflow === 'string';
|
|
63
|
+
}
|
|
60
64
|
// Defense-in-depth: the issues source filters PR-shaped items at poll
|
|
61
65
|
// time, so no NEW projection can ever have isPullRequest: true. But a
|
|
62
66
|
// pre-existing state/<workId>.json written by a pre-this-branch
|
|
@@ -105,7 +109,7 @@ export function createPolicyEngine() {
|
|
|
105
109
|
chooseAction(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
106
110
|
return chooseWorkflowAction(issue, workflow)?.action ?? null;
|
|
107
111
|
},
|
|
108
|
-
chooseRetryActionAfterHumanReply(issue) {
|
|
112
|
+
chooseRetryActionAfterHumanReply(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
109
113
|
const context = issue.context;
|
|
110
114
|
const failed = context.lastRunSentinel === failedRunnerSentinel;
|
|
111
115
|
const blocked = context.lastRunSentinel === 'BLOCKED';
|
|
@@ -118,7 +122,16 @@ export function createPolicyEngine() {
|
|
|
118
122
|
if (latestUnhandledHumanComment(issue) === undefined) {
|
|
119
123
|
return null;
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
if (typeof context.blockedFromStage !== 'string') {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return (chooseWorkflowAction({
|
|
129
|
+
...issue,
|
|
130
|
+
wake: {
|
|
131
|
+
...issue.wake,
|
|
132
|
+
stage: context.blockedFromStage,
|
|
133
|
+
},
|
|
134
|
+
}, workflow)?.action ?? null);
|
|
122
135
|
},
|
|
123
136
|
resolveApprovalTransition(issue) {
|
|
124
137
|
if (!isAwaitingApproval(issue)) {
|
|
@@ -179,6 +192,9 @@ export function createPolicyEngine() {
|
|
|
179
192
|
return false;
|
|
180
193
|
}
|
|
181
194
|
const kind = resourceUri.split(':')[1];
|
|
195
|
+
if (config.workflowSelectors.length > 0) {
|
|
196
|
+
return selectWorkflowForEvent(unresolved, config) !== null;
|
|
197
|
+
}
|
|
182
198
|
if (kind === 'issue') {
|
|
183
199
|
// Real github source stamps payload.ticket (sourceEventType
|
|
184
200
|
// 'ticket.upsert'); the fake ticketing harness stamps payload.issue
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, UNRESOLVED_WORK_ITEM_KEY, parseIssueStateRecord, } from '../domain/schema.js';
|
|
2
2
|
import { doneRunnerSentinel, stageFromLabels } from '../domain/stages.js';
|
|
3
|
-
import { builtInDefaultWorkflowDefinition, defaultWorkflowName, workflowStageVocabulary, } from '../domain/workflows.js';
|
|
3
|
+
import { builtInDefaultWorkflowDefinition, defaultWorkflowName, selectWorkflowForEvent, workflowStageVocabulary, } from '../domain/workflows.js';
|
|
4
4
|
import { isCustomCommandAction } from '../domain/custom-commands.js';
|
|
5
5
|
import { createEventEnvelope } from '../lib/event-log.js';
|
|
6
|
+
const WORKFLOW_SELECTED_EVENT = 'wake.workflow.selected';
|
|
6
7
|
function stringArrayFromPayload(value) {
|
|
7
8
|
return Array.isArray(value)
|
|
8
9
|
? value.filter((entry) => typeof entry === 'string')
|
|
@@ -37,6 +38,33 @@ function createProjectionFromIssueEvent(event, config) {
|
|
|
37
38
|
context: {},
|
|
38
39
|
});
|
|
39
40
|
}
|
|
41
|
+
async function pinSelectedWorkflow(projection, event, ctx, config) {
|
|
42
|
+
const context = projection.context;
|
|
43
|
+
if (config === undefined || typeof context.workflow === 'string') {
|
|
44
|
+
return projection;
|
|
45
|
+
}
|
|
46
|
+
const workflow = selectWorkflowForEvent(event, config);
|
|
47
|
+
if (workflow === null) {
|
|
48
|
+
return projection;
|
|
49
|
+
}
|
|
50
|
+
const selectedEvent = await ctx.appendEvent(createEventEnvelope({
|
|
51
|
+
eventId: `workflow-selected-${projection.workItemKey}`,
|
|
52
|
+
workItemKey: projection.workItemKey,
|
|
53
|
+
streamScope: 'work-item',
|
|
54
|
+
direction: 'internal',
|
|
55
|
+
sourceSystem: 'wake',
|
|
56
|
+
sourceEventType: WORKFLOW_SELECTED_EVENT,
|
|
57
|
+
sourceRefs: event.sourceRefs,
|
|
58
|
+
occurredAt: event.occurredAt,
|
|
59
|
+
ingestedAt: event.ingestedAt,
|
|
60
|
+
trigger: 'context-only',
|
|
61
|
+
payload: {
|
|
62
|
+
workflow,
|
|
63
|
+
selectedFromEventId: event.eventId,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
return applyEvent(projection, selectedEvent, ctx, config);
|
|
67
|
+
}
|
|
40
68
|
async function applyEvent(current, event, ctx, config) {
|
|
41
69
|
// The shared sentinel for events whose resource failed mint qualification
|
|
42
70
|
// (tick-runner.ts's resolveInboundEvent, spec D1'). These are durable and
|
|
@@ -55,11 +83,11 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
55
83
|
return current;
|
|
56
84
|
}
|
|
57
85
|
if (current === null) {
|
|
58
|
-
return next;
|
|
86
|
+
return pinSelectedWorkflow(next, event, ctx, config);
|
|
59
87
|
}
|
|
60
88
|
const nextStageFromLabels = stageFromLabels(next.issue.labels, configuredStagesForLabels(config));
|
|
61
89
|
const shouldReconcileStage = nextStageFromLabels !== undefined && nextStageFromLabels !== current.wake.stage;
|
|
62
|
-
|
|
90
|
+
const updated = parseIssueStateRecord({
|
|
63
91
|
...current,
|
|
64
92
|
origin: current.origin,
|
|
65
93
|
issue: next.issue,
|
|
@@ -80,10 +108,29 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
80
108
|
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
81
109
|
},
|
|
82
110
|
});
|
|
111
|
+
return pinSelectedWorkflow(updated, event, ctx, config);
|
|
83
112
|
}
|
|
84
113
|
if (current === null) {
|
|
85
114
|
return null;
|
|
86
115
|
}
|
|
116
|
+
if (event.sourceEventType === WORKFLOW_SELECTED_EVENT) {
|
|
117
|
+
const workflow = event.payload.workflow;
|
|
118
|
+
if (typeof workflow !== 'string') {
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
return parseIssueStateRecord({
|
|
122
|
+
...current,
|
|
123
|
+
context: {
|
|
124
|
+
...current.context,
|
|
125
|
+
workflow,
|
|
126
|
+
},
|
|
127
|
+
wake: {
|
|
128
|
+
...current.wake,
|
|
129
|
+
syncedAt: event.ingestedAt,
|
|
130
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
87
134
|
if (event.sourceEventType === 'fake.issue.comment.created' ||
|
|
88
135
|
event.sourceEventType === 'ticket.comment.created' ||
|
|
89
136
|
event.sourceEventType === 'ticket.comment.updated' ||
|
|
@@ -156,31 +203,38 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
156
203
|
config !== undefined &&
|
|
157
204
|
isCustomCommandAction(payload.action, config);
|
|
158
205
|
const shouldClearSession = isForwardProgression || isFailed;
|
|
206
|
+
const nextContext = {
|
|
207
|
+
...current.context,
|
|
208
|
+
lastFailureClass: payload.failureClass,
|
|
209
|
+
...(payload.handledCommentId === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { lastHandledCommentId: payload.handledCommentId }),
|
|
212
|
+
...(payload.sentinel === undefined || isCompletedCustomCommand
|
|
213
|
+
? {}
|
|
214
|
+
: { lastRunSentinel: payload.sentinel }),
|
|
215
|
+
...(payload.action === undefined || isCompletedCustomCommand
|
|
216
|
+
? {}
|
|
217
|
+
: { lastRunAction: payload.action }),
|
|
218
|
+
...(payload.sentinel === doneRunnerSentinel &&
|
|
219
|
+
payload.action !== undefined &&
|
|
220
|
+
!isCompletedCustomCommand
|
|
221
|
+
? { lastCompletedAction: payload.action }
|
|
222
|
+
: {}),
|
|
223
|
+
// Remembered so the approval path knows which action to resume or
|
|
224
|
+
// skip when a human posts /approved.
|
|
225
|
+
...(payload.sentinel === 'AWAITING_APPROVAL' && payload.action !== undefined
|
|
226
|
+
? { pendingApprovalAction: payload.action }
|
|
227
|
+
: {}),
|
|
228
|
+
};
|
|
229
|
+
if (payload.sentinel === 'BLOCKED' || payload.sentinel === 'FAILED') {
|
|
230
|
+
nextContext.blockedFromStage = current.wake.stage;
|
|
231
|
+
}
|
|
232
|
+
else if (payload.sentinel !== undefined) {
|
|
233
|
+
delete nextContext.blockedFromStage;
|
|
234
|
+
}
|
|
159
235
|
return parseIssueStateRecord({
|
|
160
236
|
...current,
|
|
161
|
-
context:
|
|
162
|
-
...current.context,
|
|
163
|
-
lastFailureClass: payload.failureClass,
|
|
164
|
-
...(payload.handledCommentId === undefined
|
|
165
|
-
? {}
|
|
166
|
-
: { lastHandledCommentId: payload.handledCommentId }),
|
|
167
|
-
...(payload.sentinel === undefined || isCompletedCustomCommand
|
|
168
|
-
? {}
|
|
169
|
-
: { lastRunSentinel: payload.sentinel }),
|
|
170
|
-
...(payload.action === undefined || isCompletedCustomCommand
|
|
171
|
-
? {}
|
|
172
|
-
: { lastRunAction: payload.action }),
|
|
173
|
-
...(payload.sentinel === doneRunnerSentinel &&
|
|
174
|
-
payload.action !== undefined &&
|
|
175
|
-
!isCompletedCustomCommand
|
|
176
|
-
? { lastCompletedAction: payload.action }
|
|
177
|
-
: {}),
|
|
178
|
-
// Remembered so the approval path knows which action to resume or
|
|
179
|
-
// skip when a human posts /approved.
|
|
180
|
-
...(payload.sentinel === 'AWAITING_APPROVAL' && payload.action !== undefined
|
|
181
|
-
? { pendingApprovalAction: payload.action }
|
|
182
|
-
: {}),
|
|
183
|
-
},
|
|
237
|
+
context: nextContext,
|
|
184
238
|
wake: {
|
|
185
239
|
...current.wake,
|
|
186
240
|
stage: payload.nextStage ?? current.wake.stage,
|
|
@@ -9,7 +9,7 @@ import { createWorkId } from '../lib/work-id.js';
|
|
|
9
9
|
import { CORRELATION_REGISTERED_EVENT, UNRESOLVED_WORK_ITEM_KEY, WORK_ITEM_CREATED_EVENT, parseRunnerArtifacts, parseRunnerResult, } from '../domain/schema.js';
|
|
10
10
|
import { maxConfiguredRunnerTimeoutMs, resolveRunnerRouting } from '../domain/runner-routing.js';
|
|
11
11
|
import { awaitingApprovalRunnerSentinel, stageLabelForStage } from '../domain/stages.js';
|
|
12
|
-
import { chooseAction as chooseWorkflowAction, isKnownWorkflowStage, workflowChangedBlockReason, workflowForProjection, workflowNameForProjection, } from '../domain/workflows.js';
|
|
12
|
+
import { chooseAction as chooseWorkflowAction, isKnownWorkflowStage, workflowChangedBlockReason, workflowForProjection, workflowLabelForWorkflowName, workflowNameForProjection, } from '../domain/workflows.js';
|
|
13
13
|
import { createEventEnvelope } from '../lib/event-log.js';
|
|
14
14
|
import { branchNameForIssue } from '../domain/branch-naming.js';
|
|
15
15
|
import { customCommandWorkspace, isCustomCommandAction } from '../domain/custom-commands.js';
|
|
@@ -222,12 +222,12 @@ export function createTickRunner(deps) {
|
|
|
222
222
|
}
|
|
223
223
|
const nextAction = policy.resolveCustomCommandRequest(projection, deps.config)?.action ??
|
|
224
224
|
policy.chooseAction(projection, workflow) ??
|
|
225
|
-
policy.chooseRetryActionAfterHumanReply(projection);
|
|
225
|
+
policy.chooseRetryActionAfterHumanReply(projection, workflow);
|
|
226
226
|
return nextAction !== null && policy.needsWakeAction(projection, workflow);
|
|
227
227
|
}
|
|
228
228
|
function createLabelsEvent(input) {
|
|
229
229
|
return createEventEnvelope({
|
|
230
|
-
eventId: `${input.runId}-labels-${input.statusLabel.replace(/[^a-z0-9]+/gi, '-')}-${input.stageLabel.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
230
|
+
eventId: `${input.runId}-labels-${input.statusLabel.replace(/[^a-z0-9]+/gi, '-')}-${input.stageLabel.replace(/[^a-z0-9]+/gi, '-')}-${input.workflowLabel.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
231
231
|
workItemKey: input.projection.workItemKey,
|
|
232
232
|
streamScope: 'work-item',
|
|
233
233
|
direction: 'outbound',
|
|
@@ -244,6 +244,7 @@ export function createTickRunner(deps) {
|
|
|
244
244
|
payload: {
|
|
245
245
|
statusLabel: input.statusLabel,
|
|
246
246
|
stageLabel: input.stageLabel,
|
|
247
|
+
workflowLabel: input.workflowLabel,
|
|
247
248
|
origin: input.projection.origin ?? 'github',
|
|
248
249
|
},
|
|
249
250
|
});
|
|
@@ -340,8 +341,11 @@ export function createTickRunner(deps) {
|
|
|
340
341
|
for (const projection of projections) {
|
|
341
342
|
const statusLabel = statusLabelForStage(projection.wake.stage);
|
|
342
343
|
const stageLabel = stageLabelForStage(projection.wake.stage);
|
|
344
|
+
const workflowLabel = workflowLabelForWorkflowName(workflowNameForProjection(projection, deps.config));
|
|
343
345
|
if (!shouldMarkPending(projection) ||
|
|
344
|
-
(hasLabel(projection, statusLabel) &&
|
|
346
|
+
(hasLabel(projection, statusLabel) &&
|
|
347
|
+
hasLabel(projection, stageLabel) &&
|
|
348
|
+
hasLabel(projection, workflowLabel))) {
|
|
345
349
|
continue;
|
|
346
350
|
}
|
|
347
351
|
await deliverOutboundEvent(createLabelsEvent({
|
|
@@ -349,6 +353,7 @@ export function createTickRunner(deps) {
|
|
|
349
353
|
runId: `pending-${projection.workItemKey}-${deps.clock.now().getTime()}`,
|
|
350
354
|
statusLabel,
|
|
351
355
|
stageLabel,
|
|
356
|
+
workflowLabel,
|
|
352
357
|
occurredAt: eventStampNow(),
|
|
353
358
|
}));
|
|
354
359
|
}
|
|
@@ -732,6 +737,7 @@ export function createTickRunner(deps) {
|
|
|
732
737
|
runId: record.runId,
|
|
733
738
|
statusLabel: 'wake:status.failed',
|
|
734
739
|
stageLabel: stageLabelForStage(updatedProjection.wake.stage),
|
|
740
|
+
workflowLabel: workflowLabelForWorkflowName(workflowNameForProjection(updatedProjection, deps.config)),
|
|
735
741
|
occurredAt: finishedAt,
|
|
736
742
|
}));
|
|
737
743
|
}
|
|
@@ -779,6 +785,7 @@ export function createTickRunner(deps) {
|
|
|
779
785
|
runId: eventId,
|
|
780
786
|
statusLabel: 'wake:status.blocked',
|
|
781
787
|
stageLabel: stageLabelForStage(projection.wake.stage),
|
|
788
|
+
workflowLabel: workflowLabelForWorkflowName(workflowNameForProjection(projection, deps.config)),
|
|
782
789
|
occurredAt,
|
|
783
790
|
}));
|
|
784
791
|
parked = true;
|
|
@@ -958,7 +965,7 @@ export function createTickRunner(deps) {
|
|
|
958
965
|
}
|
|
959
966
|
const nextAction = policy.resolveCustomCommandRequest(issue, deps.config)?.action ??
|
|
960
967
|
policy.chooseAction(issue, workflow) ??
|
|
961
|
-
policy.chooseRetryActionAfterHumanReply(issue);
|
|
968
|
+
policy.chooseRetryActionAfterHumanReply(issue, workflow);
|
|
962
969
|
return nextAction !== null && policy.needsWakeAction(issue, workflow);
|
|
963
970
|
});
|
|
964
971
|
if (candidate === undefined) {
|
|
@@ -1031,6 +1038,7 @@ export function createTickRunner(deps) {
|
|
|
1031
1038
|
runId: approvalId,
|
|
1032
1039
|
statusLabel: statusLabelForStage(nextStage),
|
|
1033
1040
|
stageLabel: stageLabelForStage(nextStage),
|
|
1041
|
+
workflowLabel: workflowLabelForWorkflowName(workflowName),
|
|
1034
1042
|
occurredAt: approvedAt,
|
|
1035
1043
|
}));
|
|
1036
1044
|
return {
|
|
@@ -1061,7 +1069,7 @@ export function createTickRunner(deps) {
|
|
|
1061
1069
|
// back to a full fresh `implement` run and lost the PR-feedback
|
|
1062
1070
|
// context).
|
|
1063
1071
|
const nextAction = policy.resolveCustomCommandRequest(candidate, deps.config)?.action ??
|
|
1064
|
-
policy.chooseRetryActionAfterHumanReply(candidate) ??
|
|
1072
|
+
policy.chooseRetryActionAfterHumanReply(candidate, workflow) ??
|
|
1065
1073
|
workflowAction?.action ??
|
|
1066
1074
|
null;
|
|
1067
1075
|
if (nextAction === null) {
|
|
@@ -1130,6 +1138,7 @@ export function createTickRunner(deps) {
|
|
|
1130
1138
|
runId,
|
|
1131
1139
|
statusLabel: 'wake:status.working',
|
|
1132
1140
|
stageLabel: stageLabelForStage(claimedStage),
|
|
1141
|
+
workflowLabel: workflowLabelForWorkflowName(workflowName),
|
|
1133
1142
|
occurredAt: eventStampNow(),
|
|
1134
1143
|
}));
|
|
1135
1144
|
try {
|
|
@@ -1288,6 +1297,7 @@ export function createTickRunner(deps) {
|
|
|
1288
1297
|
stage: nextStage ?? claimedStage,
|
|
1289
1298
|
}),
|
|
1290
1299
|
stageLabel: stageLabelForStage(nextStage ?? claimedStage),
|
|
1300
|
+
workflowLabel: workflowLabelForWorkflowName(workflowName),
|
|
1291
1301
|
occurredAt: finishedAt,
|
|
1292
1302
|
}));
|
|
1293
1303
|
if (runnerResult.failureClass !== 'quota') {
|
|
@@ -1358,6 +1368,7 @@ export function createTickRunner(deps) {
|
|
|
1358
1368
|
runId,
|
|
1359
1369
|
statusLabel: 'wake:status.failed',
|
|
1360
1370
|
stageLabel: stageLabelForStage(claimedStage),
|
|
1371
|
+
workflowLabel: workflowLabelForWorkflowName(workflowName),
|
|
1361
1372
|
occurredAt: finishedAt,
|
|
1362
1373
|
}));
|
|
1363
1374
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -316,6 +316,26 @@ const workflowDefinitionSchema = z.object({
|
|
|
316
316
|
entryStage: identifierSchema.optional(),
|
|
317
317
|
stages: z.record(identifierSchema, workflowStageSchema),
|
|
318
318
|
});
|
|
319
|
+
const workflowSelectorMatchSchema = z
|
|
320
|
+
.object({
|
|
321
|
+
kind: z.string().min(1).optional(),
|
|
322
|
+
sourceEventType: z.string().min(1).optional(),
|
|
323
|
+
repo: z.string().min(1).optional(),
|
|
324
|
+
requiredLabels: z.array(z.string()).default([]),
|
|
325
|
+
ignoredLabels: z.array(z.string()).default([]),
|
|
326
|
+
requiredAssignees: z.array(z.string()).default([]),
|
|
327
|
+
requiredAuthors: z.array(z.string()).default([]),
|
|
328
|
+
})
|
|
329
|
+
.default({
|
|
330
|
+
requiredLabels: [],
|
|
331
|
+
ignoredLabels: [],
|
|
332
|
+
requiredAssignees: [],
|
|
333
|
+
requiredAuthors: [],
|
|
334
|
+
});
|
|
335
|
+
const workflowSelectorSchema = z.object({
|
|
336
|
+
workflow: identifierSchema,
|
|
337
|
+
match: workflowSelectorMatchSchema,
|
|
338
|
+
});
|
|
319
339
|
function defaultPromptsRoot() {
|
|
320
340
|
const sourceDir = dirname(fileURLToPath(import.meta.url));
|
|
321
341
|
for (const candidate of [
|
|
@@ -515,6 +535,7 @@ export const wakeConfigSchema = z
|
|
|
515
535
|
},
|
|
516
536
|
},
|
|
517
537
|
}),
|
|
538
|
+
workflowSelectors: z.array(workflowSelectorSchema).default([]),
|
|
518
539
|
commands: z.record(identifierSchema, customCommandSchema).default({
|
|
519
540
|
ask: {
|
|
520
541
|
action: 'ask',
|
|
@@ -707,6 +728,15 @@ export const wakeConfigSchema = z
|
|
|
707
728
|
});
|
|
708
729
|
}
|
|
709
730
|
}
|
|
731
|
+
for (const [index, selector] of config.workflowSelectors.entries()) {
|
|
732
|
+
if (config.workflows[selector.workflow] === undefined) {
|
|
733
|
+
ctx.addIssue({
|
|
734
|
+
code: z.ZodIssueCode.custom,
|
|
735
|
+
path: ['workflowSelectors', index, 'workflow'],
|
|
736
|
+
message: `Workflow selector targets unknown workflow "${selector.workflow}".`,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
710
740
|
for (const [commandName, command] of commandEntries) {
|
|
711
741
|
if (reservedCommandNames.includes(commandName.toLowerCase())) {
|
|
712
742
|
ctx.addIssue({
|
|
@@ -2,6 +2,7 @@ import { stageLabelForStage } from './stages.js';
|
|
|
2
2
|
export const universalQueueStage = 'queue';
|
|
3
3
|
export const universalDoneStage = 'done';
|
|
4
4
|
export const workflowChangedBlockReason = 'workflow-changed';
|
|
5
|
+
export const wakeWorkflowLabelPrefix = 'wake:workflow.';
|
|
5
6
|
export const builtInDefaultWorkflowDefinition = {
|
|
6
7
|
stages: {
|
|
7
8
|
refine: {
|
|
@@ -36,6 +37,75 @@ export function workflowNameForProjection(projection, config) {
|
|
|
36
37
|
const context = projection.context;
|
|
37
38
|
return typeof context.workflow === 'string' ? context.workflow : defaultWorkflowName(config);
|
|
38
39
|
}
|
|
40
|
+
function stringArray(value) {
|
|
41
|
+
return Array.isArray(value)
|
|
42
|
+
? value.filter((entry) => typeof entry === 'string')
|
|
43
|
+
: [];
|
|
44
|
+
}
|
|
45
|
+
function repoFromResourceUri(resourceUri) {
|
|
46
|
+
if (resourceUri === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const [, , rest] = resourceUri.split(':', 3);
|
|
50
|
+
return rest?.split('#')[0];
|
|
51
|
+
}
|
|
52
|
+
export function workflowSelectorInputFromEvent(event) {
|
|
53
|
+
const resourceUri = event.sourceRefs.resourceUri ?? event.sourceRefs.parentResourceUri;
|
|
54
|
+
const kind = resourceUri?.split(':')[1];
|
|
55
|
+
const ticket = (event.payload.ticket ?? event.payload.issue);
|
|
56
|
+
const pr = event.payload.pr;
|
|
57
|
+
const repo = (typeof ticket?.repo === 'string' ? ticket.repo : undefined) ??
|
|
58
|
+
event.sourceRefs.repo ??
|
|
59
|
+
repoFromResourceUri(resourceUri);
|
|
60
|
+
return {
|
|
61
|
+
...(kind === undefined ? {} : { kind }),
|
|
62
|
+
sourceEventType: event.sourceEventType,
|
|
63
|
+
...(repo === undefined ? {} : { repo }),
|
|
64
|
+
labels: stringArray(ticket?.labels),
|
|
65
|
+
assignees: stringArray(ticket?.assignees),
|
|
66
|
+
...(typeof pr?.author === 'string' ? { author: pr.author } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function labelsAndAssigneesMatch(input) {
|
|
70
|
+
const labels = new Set(input.labels);
|
|
71
|
+
const assignees = new Set(input.assignees);
|
|
72
|
+
if (input.requiredLabels.some((label) => !labels.has(label))) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (input.ignoredLabels.some((label) => labels.has(label))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (input.requiredAssignees.length > 0 &&
|
|
79
|
+
!input.requiredAssignees.some((login) => assignees.has(login))) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function selectorMatches(selector, input) {
|
|
85
|
+
const match = selector.match;
|
|
86
|
+
if (match.kind !== undefined && match.kind !== input.kind) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (match.sourceEventType !== undefined && match.sourceEventType !== input.sourceEventType) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (match.repo !== undefined && match.repo !== input.repo) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!labelsAndAssigneesMatch({ ...match, labels: input.labels, assignees: input.assignees })) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (match.requiredAuthors.length > 0 &&
|
|
99
|
+
(input.author === undefined || !match.requiredAuthors.includes(input.author))) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
export function selectWorkflowForEvent(event, config) {
|
|
105
|
+
const input = workflowSelectorInputFromEvent(event);
|
|
106
|
+
const selector = config.workflowSelectors.find((candidate) => selectorMatches(candidate, input));
|
|
107
|
+
return selector?.workflow ?? null;
|
|
108
|
+
}
|
|
39
109
|
export function configuredStageNames(workflow) {
|
|
40
110
|
return Object.keys(workflow.stages);
|
|
41
111
|
}
|
|
@@ -45,6 +115,9 @@ export function workflowStageVocabulary(workflow) {
|
|
|
45
115
|
export function stageLabelsForWorkflow(workflow) {
|
|
46
116
|
return workflowStageVocabulary(workflow).map((stage) => stageLabelForStage(stage));
|
|
47
117
|
}
|
|
118
|
+
export function workflowLabelForWorkflowName(workflowName) {
|
|
119
|
+
return `${wakeWorkflowLabelPrefix}${workflowName}`;
|
|
120
|
+
}
|
|
48
121
|
export function isKnownWorkflowStage(stage, workflow) {
|
|
49
122
|
return workflowStageVocabulary(workflow).includes(stage);
|
|
50
123
|
}
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "0.1.
|
|
1
|
+
export const wakeVersion = "0.1.12+g6a4753f";
|