@atolis-hq/wake 0.3.5 → 0.3.7
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/activities/agent/agent-activity.js +2 -0
- package/dist/src/bootstrap/composition-root.js +1 -1
- package/dist/src/bootstrap/projection-runtime.js +2 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/application/agent-context-reader.js +40 -1
- package/dist/src/integrations/github/application/inbound-translator.js +1 -0
- package/dist/src/integrations/github/application/pull-request-translation.js +1 -0
- package/dist/src/integrations/github/contracts/check-evidence.js +37 -0
- package/dist/src/integrations/github/contracts/events.js +1 -0
- package/dist/src/integrations/github/infrastructure/client-reads.js +14 -0
- package/dist/src/integrations/github/infrastructure/client.js +2 -1
- package/dist/src/integrations/github/infrastructure/pr-source.js +20 -2
- package/dist/src/integrations/github/provider.js +1 -0
- package/dist/src/orchestration/application/accept-activity-outcome.js +11 -8
- package/dist/src/orchestration/application/accept-signal.js +4 -1
- package/dist/src/orchestration/application/advance-workflow.js +42 -15
- package/dist/src/orchestration/application/orchestration-repository.js +1 -1
- package/dist/src/orchestration/application/orchestration-service.js +6 -5
- package/dist/src/orchestration/application/request-child.js +4 -1
- package/dist/src/orchestration/application/start-workflow.js +42 -7
- package/dist/src/orchestration/application/watch-reactor.js +1 -1
- package/dist/src/orchestration/application/workflow-definition-registry.js +108 -0
- package/dist/src/orchestration/contracts/config.js +5 -0
- package/dist/src/orchestration/contracts/event-decoder.js +22 -0
- package/dist/src/orchestration/contracts/events.js +1 -0
- package/dist/src/orchestration/contracts/streams.js +5 -0
- package/dist/src/orchestration/domain/activation-policy.js +3 -0
- package/dist/src/orchestration/domain/compiler.js +5 -1
- package/dist/src/orchestration/domain/workflow-instance-events.js +3 -0
- package/dist/src/orchestration/domain/workflow-instance.js +3 -0
- package/dist/src/orchestration/index.js +1 -0
- package/package.json +1 -1
|
@@ -97,6 +97,7 @@ async function buildUntrustedContext(workItemId, contextReader) {
|
|
|
97
97
|
issueTitle: context.title,
|
|
98
98
|
issueBody: context.body,
|
|
99
99
|
comments: context.comments,
|
|
100
|
+
...(context.pullRequest === undefined ? {} : { pullRequest: context.pullRequest }),
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
function untrustedDataBlock(context) {
|
|
@@ -108,6 +109,7 @@ function untrustedDataBlock(context) {
|
|
|
108
109
|
escapeUntrustedJson(JSON.stringify({
|
|
109
110
|
issue: { title: context.issueTitle, body: context.issueBody },
|
|
110
111
|
comments: context.comments,
|
|
112
|
+
...(context.pullRequest === undefined ? {} : { pullRequest: context.pullRequest }),
|
|
111
113
|
}, null, 2)),
|
|
112
114
|
'</wake-untrusted-data>',
|
|
113
115
|
].join('\n');
|
|
@@ -46,7 +46,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
46
46
|
name,
|
|
47
47
|
compileWorkflow(name, definition, activities, Object.keys(config.orchestration.workflows)),
|
|
48
48
|
]));
|
|
49
|
-
const orchestration = createOrchestrationService(journal, work, definitions);
|
|
49
|
+
const orchestration = createOrchestrationService(journal, work, definitions, projections);
|
|
50
50
|
const workspaces = new GitWorkspaceProvider(paths.workspacesRoot, {
|
|
51
51
|
async cloneLocator(id) {
|
|
52
52
|
const resource = await resources.get(resourceId(id));
|
|
@@ -2,7 +2,7 @@ import { activityProjectionDefinitions } from '../activities/index.js';
|
|
|
2
2
|
import { controlPlaneProjectionDefinitions } from '../control-plane/index.js';
|
|
3
3
|
import { executionProjection } from '../execution/index.js';
|
|
4
4
|
import { deliveryProjectionDefinitions } from '../integrations/index.js';
|
|
5
|
-
import { orchestrationProjection } from '../orchestration/index.js';
|
|
5
|
+
import { orchestrationProjection, workflowDefinitionsProjection } from '../orchestration/index.js';
|
|
6
6
|
import { ProjectionRunner } from '../persistence/index.js';
|
|
7
7
|
import { resourceCorrelationProjection, resourceProjection, resourcesByExternalKeyProjection, workCorrelationsProjection, } from '../resources/index.js';
|
|
8
8
|
import { workProjection } from '../work/index.js';
|
|
@@ -18,6 +18,7 @@ export const runtimeProjectionDefinitions = [
|
|
|
18
18
|
...deliveryProjectionDefinitions,
|
|
19
19
|
...controlPlaneProjectionDefinitions,
|
|
20
20
|
orchestrationProjection,
|
|
21
|
+
workflowDefinitionsProjection,
|
|
21
22
|
executionProjection,
|
|
22
23
|
boardProjection,
|
|
23
24
|
analyticsProjection,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PullRequestCheckState, } from '../../../activities/index.js';
|
|
2
|
+
import { BuiltInResourceKind, ResourceCorrelationRole, } from '../../../resources/index.js';
|
|
2
3
|
import { adapterId } from '../../contracts/identifiers.js';
|
|
3
4
|
import { integrationStream } from '../../contracts/streams.js';
|
|
5
|
+
import { boundedDiagnosticEvidence } from '../contracts/check-evidence.js';
|
|
4
6
|
import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
|
|
5
7
|
import { createCommentHistoryReader } from './comment-history-reader.js';
|
|
6
8
|
export function createGitHubAgentContextReader(journal, resources) {
|
|
@@ -11,6 +13,7 @@ export function createGitHubAgentContextReader(journal, resources) {
|
|
|
11
13
|
return {
|
|
12
14
|
...(await currentWorkItemContent(journal, resources, workItemId)),
|
|
13
15
|
comments,
|
|
16
|
+
...pullRequestContextField(await currentPullRequestContext(journal, resources, workItemId)),
|
|
14
17
|
};
|
|
15
18
|
},
|
|
16
19
|
};
|
|
@@ -28,6 +31,38 @@ async function currentWorkItemContent(journal, resources, workItemId) {
|
|
|
28
31
|
return latestWorkObservedContent(await journal.readStream(integrationStream(adapter)), adapter, resource.externalKey.key);
|
|
29
32
|
}
|
|
30
33
|
const emptyWorkItemContent = { title: '', body: '' };
|
|
34
|
+
async function currentPullRequestContext(journal, resources, workItemId) {
|
|
35
|
+
let current;
|
|
36
|
+
for (const correlation of await resources.correlationsForWork(workItemId)) {
|
|
37
|
+
const resource = await resources.get(correlation.resourceId);
|
|
38
|
+
if (resource?.kind !== BuiltInResourceKind.PullRequest)
|
|
39
|
+
continue;
|
|
40
|
+
const adapter = parseAdapterId(resource.externalKey.adapter);
|
|
41
|
+
if (adapter === null)
|
|
42
|
+
continue;
|
|
43
|
+
for (const event of await journal.readStream(integrationStream(adapter))) {
|
|
44
|
+
const observed = selectGitHubAdapterEvent(event);
|
|
45
|
+
if (!isCurrentPullRequestObservedEvent(observed, adapter, resource.externalKey.key))
|
|
46
|
+
continue;
|
|
47
|
+
if (current === undefined || event.globalPosition > current.position)
|
|
48
|
+
current = { context: pullRequestContext(observed.payload), position: event.globalPosition };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return current?.context;
|
|
52
|
+
}
|
|
53
|
+
function pullRequestContext(payload) {
|
|
54
|
+
return {
|
|
55
|
+
checks: payload.checks ?? PullRequestCheckState.Unknown,
|
|
56
|
+
checkRuns: rawEvidenceList(payload.raw.checkRuns),
|
|
57
|
+
statuses: rawEvidenceList(payload.raw.statuses),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function rawEvidenceList(value) {
|
|
61
|
+
return Array.isArray(value) ? boundedDiagnosticEvidence(value) : [];
|
|
62
|
+
}
|
|
63
|
+
function pullRequestContextField(context) {
|
|
64
|
+
return context === undefined ? {} : { pullRequest: context };
|
|
65
|
+
}
|
|
31
66
|
function latestWorkObservedContent(events, adapter, externalKey) {
|
|
32
67
|
let current;
|
|
33
68
|
for (const event of events) {
|
|
@@ -44,6 +79,10 @@ function isCurrentWorkObservedEvent(observed, adapter, externalKey) {
|
|
|
44
79
|
observed.source.id === adapter &&
|
|
45
80
|
observed.payload.externalKey === externalKey);
|
|
46
81
|
}
|
|
82
|
+
function isCurrentPullRequestObservedEvent(observed, adapter, externalKey) {
|
|
83
|
+
return (isCurrentWorkObservedEvent(observed, adapter, externalKey) &&
|
|
84
|
+
observed.payload.kind === 'pull-request');
|
|
85
|
+
}
|
|
47
86
|
function parseAdapterId(value) {
|
|
48
87
|
try {
|
|
49
88
|
return adapterId(value);
|
|
@@ -138,6 +138,7 @@ export class InboundTranslator {
|
|
|
138
138
|
BuiltInResourceCapability.Commentable,
|
|
139
139
|
BuiltInResourceCapability.Reviewable,
|
|
140
140
|
BuiltInResourceCapability.Revisioned,
|
|
141
|
+
BuiltInResourceCapability.ChangedFiles,
|
|
141
142
|
]
|
|
142
143
|
: [BuiltInResourceCapability.Commentable],
|
|
143
144
|
objective: payload.title,
|
|
@@ -6,5 +6,6 @@ export function observePullRequest(resourceId, workItemId, payload) {
|
|
|
6
6
|
headRevision: payload.headRevision ?? payload.revision,
|
|
7
7
|
baseRevision: payload.baseRevision ?? 'unknown',
|
|
8
8
|
checks: payload.checks ?? 'unknown',
|
|
9
|
+
...(payload.changedFiles === undefined ? {} : { changedFiles: payload.changedFiles }),
|
|
9
10
|
};
|
|
10
11
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const diagnosticKeys = [
|
|
2
|
+
'name',
|
|
3
|
+
'status',
|
|
4
|
+
'conclusion',
|
|
5
|
+
'started_at',
|
|
6
|
+
'completed_at',
|
|
7
|
+
'details_url',
|
|
8
|
+
'html_url',
|
|
9
|
+
'context',
|
|
10
|
+
'state',
|
|
11
|
+
'target_url',
|
|
12
|
+
];
|
|
13
|
+
const maxEvidenceEntries = 20;
|
|
14
|
+
const maxEvidenceBytes = 12_000;
|
|
15
|
+
export function boundedDiagnosticEvidence(evidence) {
|
|
16
|
+
const bounded = [];
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
for (const value of evidence) {
|
|
19
|
+
if (bounded.length === maxEvidenceEntries)
|
|
20
|
+
break;
|
|
21
|
+
if (!isRecord(value))
|
|
22
|
+
continue;
|
|
23
|
+
const diagnostic = diagnosticEvidence(value);
|
|
24
|
+
const entryBytes = Buffer.byteLength(JSON.stringify(diagnostic), 'utf8');
|
|
25
|
+
if (bytes + entryBytes > maxEvidenceBytes)
|
|
26
|
+
break;
|
|
27
|
+
bounded.push(diagnostic);
|
|
28
|
+
bytes += entryBytes;
|
|
29
|
+
}
|
|
30
|
+
return bounded;
|
|
31
|
+
}
|
|
32
|
+
function diagnosticEvidence(entry) {
|
|
33
|
+
return Object.fromEntries(diagnosticKeys.flatMap((key) => typeof entry[key] === 'string' || entry[key] === null ? [[key, entry[key]]] : []));
|
|
34
|
+
}
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
@@ -67,6 +67,7 @@ const eventSchema = z.discriminatedUnion('eventType', [
|
|
|
67
67
|
actor: actorSchema,
|
|
68
68
|
labels: z.array(z.string()).readonly().optional(),
|
|
69
69
|
assignees: z.array(z.string()).readonly().optional(),
|
|
70
|
+
changedFiles: z.array(z.string()).readonly().optional(),
|
|
70
71
|
raw: rawSchema,
|
|
71
72
|
})
|
|
72
73
|
.strict(),
|
|
@@ -89,6 +89,20 @@ export async function listReviewComments(octokit, cache, owner, repo, pullNumber
|
|
|
89
89
|
...(comment.side === undefined ? {} : { side: comment.side }),
|
|
90
90
|
}));
|
|
91
91
|
}
|
|
92
|
+
export async function listPullRequestFiles(octokit, cache, owner, repo, pullNumber) {
|
|
93
|
+
const files = await fetchPaginatedWithEtag({
|
|
94
|
+
cache,
|
|
95
|
+
key: `pull-files:${owner}/${repo}#${pullNumber}`,
|
|
96
|
+
pages: (headers) => octokit.paginate.iterator(octokit.rest.pulls.listFiles, {
|
|
97
|
+
owner,
|
|
98
|
+
repo,
|
|
99
|
+
pull_number: pullNumber,
|
|
100
|
+
per_page: 100,
|
|
101
|
+
...(headers === undefined ? {} : { headers }),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
return files.map((file) => file.filename);
|
|
105
|
+
}
|
|
92
106
|
export function listCheckRunsForRef(octokit, cache, owner, repo, ref) {
|
|
93
107
|
return fetchPaginatedWithEtag({
|
|
94
108
|
cache,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Octokit } from '@octokit/rest';
|
|
2
2
|
import { MergeMethod } from '../../../activities/index.js';
|
|
3
3
|
import { GitHubOutboundAction } from '../contracts/vocabulary.js';
|
|
4
|
-
import { branch, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
|
|
4
|
+
import { branch, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequestFiles, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
|
|
5
5
|
import { createEtagCache } from './etag-cache.js';
|
|
6
6
|
// Octokit's request-log plugin reports every non-2xx response through this
|
|
7
7
|
// callback, including the expected 304 responses produced by conditional ETag
|
|
@@ -50,6 +50,7 @@ export function createGitHubClient(token) {
|
|
|
50
50
|
getPullRequest: (owner, repo, pullNumber) => getPullRequest(octokit, cache, owner, repo, pullNumber),
|
|
51
51
|
listReviews: (owner, repo, pullNumber, pageSize) => listReviews(octokit, cache, owner, repo, pullNumber, pageSize),
|
|
52
52
|
listCheckRunsForRef: (owner, repo, ref) => listCheckRunsForRef(octokit, cache, owner, repo, ref),
|
|
53
|
+
listPullRequestFiles: (owner, repo, pullNumber) => listPullRequestFiles(octokit, cache, owner, repo, pullNumber),
|
|
53
54
|
getCombinedStatusForRef: (owner, repo, ref) => getCombinedStatusForRef(octokit, cache, owner, repo, ref),
|
|
54
55
|
branch: (owner, repo, name) => branch(octokit, cache, owner, repo, name),
|
|
55
56
|
deliver: (command) => deliver(octokit, command),
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { PullRequestCheckState, PullRequestState, ReviewActorKind, } from '../../../activities/index.js';
|
|
3
3
|
import { createEventDraft, EventActorKind, EventSourceKind } from '../../../kernel/index.js';
|
|
4
4
|
import { integrationStream } from '../../contracts/streams.js';
|
|
5
|
+
import { boundedDiagnosticEvidence } from '../contracts/check-evidence.js';
|
|
5
6
|
import { GitHubEventType } from '../contracts/events.js';
|
|
6
7
|
import { formatGitHubResourceKey } from '../contracts/external-key.js';
|
|
7
8
|
import { gitHubAssigneeLogins, gitHubLabelNames, } from '../contracts/payloads.js';
|
|
@@ -16,11 +17,15 @@ export function createGitHubPullRequestSource(input) {
|
|
|
16
17
|
return mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
|
|
17
18
|
signal.throwIfAborted();
|
|
18
19
|
const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
|
|
19
|
-
const evidence = await
|
|
20
|
+
const [evidence, changedFiles] = await Promise.all([
|
|
21
|
+
readCheckEvidence(input.client, owner, repo, headRevision),
|
|
22
|
+
readChangedFiles(input.client, owner, repo, pullRequest.number),
|
|
23
|
+
]);
|
|
20
24
|
return pullRequestObservation({
|
|
21
25
|
repository: input.repository,
|
|
22
26
|
pullRequest,
|
|
23
27
|
evidence,
|
|
28
|
+
changedFiles,
|
|
24
29
|
...(input.adapter === undefined ? {} : { adapter: input.adapter }),
|
|
25
30
|
});
|
|
26
31
|
});
|
|
@@ -53,7 +58,12 @@ function pullRequestObservation(input) {
|
|
|
53
58
|
},
|
|
54
59
|
labels: gitHubLabelNames(pullRequest),
|
|
55
60
|
assignees: gitHubAssigneeLogins(pullRequest),
|
|
56
|
-
|
|
61
|
+
...(input.changedFiles === undefined ? {} : { changedFiles: input.changedFiles }),
|
|
62
|
+
raw: {
|
|
63
|
+
number: pullRequest.number,
|
|
64
|
+
checkRuns: boundedDiagnosticEvidence(input.evidence.checkRuns),
|
|
65
|
+
statuses: boundedDiagnosticEvidence(input.evidence.statuses),
|
|
66
|
+
},
|
|
57
67
|
};
|
|
58
68
|
const fingerprint = evidenceFingerprint(payload, input.evidence);
|
|
59
69
|
return createEventDraft({
|
|
@@ -93,6 +103,14 @@ async function readCheckEvidence(client, owner, repo, headRevision) {
|
|
|
93
103
|
return { available: false, checkRuns: [], statuses: [] };
|
|
94
104
|
}
|
|
95
105
|
}
|
|
106
|
+
async function readChangedFiles(client, owner, repo, pullNumber) {
|
|
107
|
+
try {
|
|
108
|
+
return await client.listPullRequestFiles(owner, repo, pullNumber);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
96
114
|
async function mapConcurrent(values, concurrency, transform) {
|
|
97
115
|
const output = new Array(values.length);
|
|
98
116
|
let nextIndex = 0;
|
|
@@ -54,6 +54,7 @@ export const gitHubProviderDefinition = {
|
|
|
54
54
|
BuiltInResourceCapability.Approvable,
|
|
55
55
|
BuiltInResourceCapability.Mergeable,
|
|
56
56
|
BuiltInResourceCapability.Revisioned,
|
|
57
|
+
BuiltInResourceCapability.ChangedFiles,
|
|
57
58
|
],
|
|
58
59
|
...(pullRequest.head.sha === undefined ? {} : { revision: pullRequest.head.sha }),
|
|
59
60
|
};
|
|
@@ -11,14 +11,17 @@ export class AcceptActivityOutcome {
|
|
|
11
11
|
}
|
|
12
12
|
async execute(command, context) {
|
|
13
13
|
const loaded = await this.repository.loadRequired(command.workflowInstanceId);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const definition = await this.workflows.definitionForOperation(loaded.view, loaded.sequence, context);
|
|
15
|
+
if (definition !== null) {
|
|
16
|
+
const decision = decideActivityOutcome(definition, loaded.view, {
|
|
17
|
+
...command,
|
|
18
|
+
outcome: orchestrationActivityOutcome(command.outcome),
|
|
19
|
+
occurredAt: context.occurredAt,
|
|
20
|
+
causationId: context.commandId,
|
|
21
|
+
});
|
|
22
|
+
if (decision.kind === 'append')
|
|
23
|
+
await this.repository.append(command.workflowInstanceId, loaded.sequence, decision.events);
|
|
24
|
+
}
|
|
22
25
|
await this.reconcileChildCompletions(context);
|
|
23
26
|
return (await this.repository.loadRequired(command.workflowInstanceId)).view;
|
|
24
27
|
}
|
|
@@ -22,7 +22,10 @@ export class AcceptSignal {
|
|
|
22
22
|
async execute(workflowInstanceId, signal, context) {
|
|
23
23
|
const loaded = await this.repository.loadRequired(workflowInstanceId);
|
|
24
24
|
const item = await this.work.get(loaded.view.workItemId);
|
|
25
|
-
const
|
|
25
|
+
const definition = await this.workflows.definitionForOperation(loaded.view, loaded.sequence, context);
|
|
26
|
+
if (definition === null)
|
|
27
|
+
return (await this.repository.loadRequired(workflowInstanceId)).view;
|
|
28
|
+
const decision = decideSignal(definition, loaded.view, {
|
|
26
29
|
signal,
|
|
27
30
|
occurredAt: context.occurredAt,
|
|
28
31
|
causationId: context.commandId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ActivityEventType, PullRequestCheckState, selectActivityEvent, } from '../../activities/index.js';
|
|
1
2
|
import { EventSourceKind, createEventDraft } from '../../kernel/index.js';
|
|
2
3
|
import { OrchestrationEventType } from '../contracts/events.js';
|
|
3
4
|
import { commandName, workflowInstanceId, } from '../contracts/identifiers.js';
|
|
@@ -20,7 +21,10 @@ export class AdvanceWorkflow {
|
|
|
20
21
|
}
|
|
21
22
|
async requestSupplementalActivity(id, request, context) {
|
|
22
23
|
const loaded = await this.repository.loadRequired(id);
|
|
23
|
-
const
|
|
24
|
+
const definition = await this.workflows.definitionForOperation(loaded.view, loaded.sequence, context);
|
|
25
|
+
if (definition === null)
|
|
26
|
+
return (await this.repository.loadRequired(id)).view;
|
|
27
|
+
const configured = definition.commands[commandName(request.command)];
|
|
24
28
|
if (configured === undefined)
|
|
25
29
|
throw new Error(`Unknown supplemental command: ${request.command}`);
|
|
26
30
|
if (!isAuthorisedActor(configured.allowedActors, context.actor.kind))
|
|
@@ -111,7 +115,10 @@ export class AdvanceWorkflow {
|
|
|
111
115
|
throw new OperatorRetryIneligibleError('WorkflowInstance does not exist');
|
|
112
116
|
if (loaded.view.operatorRetryCommandIds.includes(context.commandId))
|
|
113
117
|
return loaded.view;
|
|
114
|
-
const
|
|
118
|
+
const definition = await this.workflows.definitionForOperation(loaded.view, loaded.sequence, context);
|
|
119
|
+
if (definition === null)
|
|
120
|
+
return (await this.repository.loadRequired(id)).view;
|
|
121
|
+
const decision = decideOperatorRetry(definition, loaded.view, {
|
|
115
122
|
commandId: context.commandId,
|
|
116
123
|
occurredAt: context.occurredAt,
|
|
117
124
|
causationId: context.commandId,
|
|
@@ -133,28 +140,48 @@ export class AdvanceWorkflow {
|
|
|
133
140
|
return (await this.repository.load(id)).view;
|
|
134
141
|
}
|
|
135
142
|
async listPendingActivations(workItemId) {
|
|
136
|
-
return (await this.
|
|
137
|
-
.filter((view) => view
|
|
138
|
-
view.status === WorkflowStatus.Active &&
|
|
143
|
+
return (await this.listAllLoaded())
|
|
144
|
+
.filter(({ view }) => view.status === WorkflowStatus.Active &&
|
|
139
145
|
view.pendingActivation !== undefined &&
|
|
140
146
|
(workItemId === undefined || view.workItemId === workItemId))
|
|
141
|
-
.map((view) => ({ workflow: view, activation: view.pendingActivation }));
|
|
147
|
+
.map(({ view }) => ({ workflow: view, activation: view.pendingActivation }));
|
|
142
148
|
}
|
|
143
149
|
async listWaiting(signalKind) {
|
|
144
|
-
return (await this.
|
|
145
|
-
(
|
|
150
|
+
return (await this.listAllLoaded())
|
|
151
|
+
.filter(({ view }) => view.status === WorkflowStatus.Waiting &&
|
|
152
|
+
(signalKind === undefined || view.waitingFor?.signalKind === signalKind))
|
|
153
|
+
.map(({ view }) => view);
|
|
146
154
|
}
|
|
147
155
|
async listAll() {
|
|
148
|
-
return (await this.
|
|
156
|
+
return (await this.listAllLoaded()).map(({ view }) => view);
|
|
149
157
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
// One shared load per live instance, reused for both the watch match and
|
|
159
|
+
// (when a match needs blocking) the append sequence — avoids reloading
|
|
160
|
+
// every instance a second time just to recover its sequence.
|
|
161
|
+
async listAllLoaded() {
|
|
162
|
+
return (await this.repository.list()).filter((loaded) => loaded.view !== null);
|
|
163
|
+
}
|
|
164
|
+
async listWatchMatches(event, context) {
|
|
165
|
+
const matches = await Promise.all((await this.listAllLoaded()).map(async ({ view: parent, sequence }) => {
|
|
166
|
+
const definition = context === undefined
|
|
167
|
+
? await this.workflows.definitionFor(parent)
|
|
168
|
+
: await this.workflows.definitionForOperation(parent, sequence, context);
|
|
169
|
+
if (definition === null)
|
|
170
|
+
return [];
|
|
153
171
|
return definition.watches
|
|
154
|
-
.filter((watch) => watch.on?.events.includes(eventType) === true &&
|
|
172
|
+
.filter((watch) => watch.on?.events.includes(event.eventType) === true &&
|
|
155
173
|
watch.while.stages.includes(parent.currentStage) &&
|
|
156
|
-
watch.while.statuses.some((status) => status === parent.status)
|
|
174
|
+
watch.while.statuses.some((status) => status === parent.status) &&
|
|
175
|
+
matchesWatchPredicate(watch.where, event))
|
|
157
176
|
.map((watch) => ({ parent, watch }));
|
|
158
|
-
});
|
|
177
|
+
}));
|
|
178
|
+
return matches.flat();
|
|
159
179
|
}
|
|
160
180
|
}
|
|
181
|
+
function matchesWatchPredicate(predicate, event) {
|
|
182
|
+
if (predicate === undefined)
|
|
183
|
+
return true;
|
|
184
|
+
const activityEvent = selectActivityEvent(event);
|
|
185
|
+
return (activityEvent?.eventType === ActivityEventType.PrChecksChanged &&
|
|
186
|
+
activityEvent.payload.checks === PullRequestCheckState.Failing);
|
|
187
|
+
}
|
|
@@ -28,7 +28,7 @@ export class OrchestrationRepository {
|
|
|
28
28
|
const ids = new Set((await this.journal.readAll(0))
|
|
29
29
|
.filter((event) => isWorkflowInstanceStream(event.stream))
|
|
30
30
|
.map((event) => event.stream.id));
|
|
31
|
-
return Promise.all([...ids].map(
|
|
31
|
+
return Promise.all([...ids].map((id) => this.load(id)));
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
function isWorkflowEvent(event) {
|
|
@@ -6,16 +6,17 @@ import { GroupBudgetRecorder } from './group-budget-recorder.js';
|
|
|
6
6
|
import { OrchestrationRepository } from './orchestration-repository.js';
|
|
7
7
|
import { RequestChild } from './request-child.js';
|
|
8
8
|
import { StartWorkflow } from './start-workflow.js';
|
|
9
|
+
import { WorkflowDefinitionRegistry } from './workflow-definition-registry.js';
|
|
9
10
|
export class OrchestrationService {
|
|
10
11
|
startWorkflow;
|
|
11
12
|
acceptActivityOutcome;
|
|
12
13
|
acceptWorkflowSignal;
|
|
13
14
|
advanceWorkflow;
|
|
14
15
|
childWorkflows;
|
|
15
|
-
constructor(journal, work, definitions) {
|
|
16
|
+
constructor(journal, work, definitions, projections) {
|
|
16
17
|
const repository = new OrchestrationRepository(journal);
|
|
17
18
|
const claims = new CoordinationClaims(journal);
|
|
18
|
-
this.startWorkflow = new StartWorkflow(repository, claims, work, definitions);
|
|
19
|
+
this.startWorkflow = new StartWorkflow(repository, claims, work, new WorkflowDefinitionRegistry(journal, projections, definitions));
|
|
19
20
|
this.acceptWorkflowSignal = new AcceptSignal(repository, this.startWorkflow, work);
|
|
20
21
|
this.advanceWorkflow = new AdvanceWorkflow(repository, this.startWorkflow);
|
|
21
22
|
this.childWorkflows = new RequestChild(repository, claims, new GroupBudgetRecorder(journal), this.startWorkflow, this.advanceWorkflow);
|
|
@@ -75,8 +76,8 @@ export class OrchestrationService {
|
|
|
75
76
|
isCausalRepeat(workflowInstanceId, triggerId, causalCycleId, requestId) {
|
|
76
77
|
return this.childWorkflows.isCausalRepeat(workflowInstanceId, triggerId, causalCycleId, requestId);
|
|
77
78
|
}
|
|
78
|
-
listWatchMatches(
|
|
79
|
-
return this.advanceWorkflow.listWatchMatches(
|
|
79
|
+
listWatchMatches(event, context) {
|
|
80
|
+
return this.advanceWorkflow.listWatchMatches(event, context);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
export const createOrchestrationService = (journal, work, definitions) => new OrchestrationService(journal, work, definitions);
|
|
83
|
+
export const createOrchestrationService = (journal, work, definitions, projections) => new OrchestrationService(journal, work, definitions, projections);
|
|
@@ -94,7 +94,10 @@ export class RequestChild {
|
|
|
94
94
|
childWorkflowInstanceId: child.workflowInstanceId,
|
|
95
95
|
requestId: metadata.requestId,
|
|
96
96
|
};
|
|
97
|
-
const
|
|
97
|
+
const definition = await this.workflows.definitionForOperation(parent.view, parent.sequence, context);
|
|
98
|
+
if (definition === null)
|
|
99
|
+
return;
|
|
100
|
+
const decision = decideSignal(definition, parent.view, {
|
|
98
101
|
signal,
|
|
99
102
|
occurredAt: context.occurredAt,
|
|
100
103
|
causationId: context.commandId,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { createEventDraft, EventSourceKind } from '../../kernel/index.js';
|
|
1
2
|
import { WorkStatus } from '../../work/index.js';
|
|
2
|
-
import {
|
|
3
|
+
import { OrchestrationEventType } from '../contracts/events.js';
|
|
4
|
+
import { workflowInstanceStream } from '../contracts/streams.js';
|
|
5
|
+
import { WorkflowInstanceKind, WorkflowStatus } from '../contracts/vocabulary.js';
|
|
3
6
|
import { validateChildProvenance } from '../domain/child-policy.js';
|
|
4
7
|
import { startInstance } from '../domain/interpreter.js';
|
|
8
|
+
import { WorkflowDefinitionUnavailableError } from './workflow-definition-registry.js';
|
|
9
|
+
const TERMINAL_OR_BLOCKED_STATUSES = new Set([
|
|
10
|
+
WorkflowStatus.Blocked,
|
|
11
|
+
WorkflowStatus.Completed,
|
|
12
|
+
WorkflowStatus.Superseded,
|
|
13
|
+
]);
|
|
5
14
|
export class StartWorkflow {
|
|
6
15
|
repository;
|
|
7
16
|
claims;
|
|
@@ -17,7 +26,7 @@ export class StartWorkflow {
|
|
|
17
26
|
const item = await this.work.get(command.workItemId);
|
|
18
27
|
if (item === null || item.state !== WorkStatus.Open)
|
|
19
28
|
throw new Error('WorkItem must exist and be open');
|
|
20
|
-
const definition = this.
|
|
29
|
+
const { definition, fingerprint } = this.definitions.currentDefinition(command.workflowName);
|
|
21
30
|
const existing = await this.repository.load(command.workflowInstanceId);
|
|
22
31
|
if (existing.view !== null)
|
|
23
32
|
return existing.view;
|
|
@@ -31,9 +40,11 @@ export class StartWorkflow {
|
|
|
31
40
|
parent.view.orchestrationGroupId !== command.orchestrationGroupId)
|
|
32
41
|
throw new Error('Child workflow must share its parent WorkItem and orchestration group');
|
|
33
42
|
}
|
|
43
|
+
await this.definitions.register(command.workflowName, fingerprint, definition, context);
|
|
34
44
|
const decision = startInstance({
|
|
35
45
|
...command,
|
|
36
46
|
definition,
|
|
47
|
+
workflowDefinitionFingerprint: fingerprint,
|
|
37
48
|
occurredAt: context.occurredAt,
|
|
38
49
|
correlationId: context.correlationId,
|
|
39
50
|
causationId: context.commandId,
|
|
@@ -42,10 +53,34 @@ export class StartWorkflow {
|
|
|
42
53
|
await this.repository.append(command.workflowInstanceId, 0, decision.events);
|
|
43
54
|
return (await this.repository.loadRequired(command.workflowInstanceId)).view;
|
|
44
55
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
definitionFor(view) {
|
|
57
|
+
return this.definitions.resolve(view);
|
|
58
|
+
}
|
|
59
|
+
async definitionForOperation(view, sequence, context) {
|
|
60
|
+
try {
|
|
61
|
+
return await this.definitions.resolve(view);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!(error instanceof WorkflowDefinitionUnavailableError))
|
|
65
|
+
throw error;
|
|
66
|
+
if (!TERMINAL_OR_BLOCKED_STATUSES.has(view.status))
|
|
67
|
+
await this.repository.append(view.workflowInstanceId, sequence, [
|
|
68
|
+
createEventDraft({
|
|
69
|
+
// workflowInstanceId is included because listWatchMatches shares one
|
|
70
|
+
// CommandContext across every matched parent in a batch; commandId
|
|
71
|
+
// alone would collide when two instances block in the same pass.
|
|
72
|
+
eventId: `${context.commandId}:${view.workflowInstanceId}:${OrchestrationEventType.InstanceBlocked}`,
|
|
73
|
+
eventType: OrchestrationEventType.InstanceBlocked,
|
|
74
|
+
occurredAt: context.occurredAt,
|
|
75
|
+
correlationId: context.correlationId,
|
|
76
|
+
causationId: context.commandId,
|
|
77
|
+
actor: context.actor,
|
|
78
|
+
source: { kind: EventSourceKind.Internal, id: 'orchestration-service' },
|
|
79
|
+
stream: workflowInstanceStream(view.workflowInstanceId),
|
|
80
|
+
payload: { reason: 'workflow-definition-unavailable' },
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
50
85
|
}
|
|
51
86
|
}
|
|
@@ -8,7 +8,7 @@ export function createWatchReactor(orchestration, journal, checkpoints, runs) {
|
|
|
8
8
|
async react(event, context) {
|
|
9
9
|
const causalCycle = orchestrationCausalCycleId(selectOrchestrationEvent(event));
|
|
10
10
|
const sourceWorkflowInstanceId = await resolveRunWorkflowInstanceId(event, runs);
|
|
11
|
-
for (const match of await orchestration.listWatchMatches(event
|
|
11
|
+
for (const match of await orchestration.listWatchMatches(event, context)) {
|
|
12
12
|
if (sourceWorkflowInstanceId !== undefined &&
|
|
13
13
|
match.parent.workflowInstanceId !== sourceWorkflowInstanceId)
|
|
14
14
|
continue;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createEventDraft, EventActorKind, EventSourceKind, WrongExpectedSequenceError, } from '../../kernel/index.js';
|
|
3
|
+
import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
|
|
4
|
+
import { OrchestrationEventType } from '../contracts/events.js';
|
|
5
|
+
import { OrchestrationStreamKind, workflowDefinitionsStream } from '../contracts/streams.js';
|
|
6
|
+
export const workflowDefinitionFingerprint = (definition) => createHash('sha256').update(JSON.stringify(definition)).digest('hex');
|
|
7
|
+
export const workflowDefinitionKey = (name, fingerprint) => `${name}:${fingerprint}`;
|
|
8
|
+
export const workflowDefinitionsProjection = {
|
|
9
|
+
name: OrchestrationStreamKind.WorkflowDefinitions,
|
|
10
|
+
select(event) {
|
|
11
|
+
const owned = selectOrchestrationEvent(event);
|
|
12
|
+
return owned?.eventType === OrchestrationEventType.WorkflowDefinitionRegistered
|
|
13
|
+
? { key: workflowDefinitionKey(owned.payload.workflowName, owned.payload.fingerprint) }
|
|
14
|
+
: null;
|
|
15
|
+
},
|
|
16
|
+
initial: () => undefined,
|
|
17
|
+
project(_previous, event) {
|
|
18
|
+
const owned = selectOrchestrationEvent(event);
|
|
19
|
+
if (owned?.eventType !== OrchestrationEventType.WorkflowDefinitionRegistered)
|
|
20
|
+
throw new Error('Expected workflow definition registration');
|
|
21
|
+
return owned.payload.compiledDefinition;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export class WorkflowDefinitionUnavailableError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('workflow-definition-unavailable');
|
|
27
|
+
this.name = 'WorkflowDefinitionUnavailableError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class WorkflowDefinitionRegistry {
|
|
31
|
+
journal;
|
|
32
|
+
projections;
|
|
33
|
+
current;
|
|
34
|
+
// WorkflowDefinitionRegistered events are never retracted, so once a
|
|
35
|
+
// (name, fingerprint) pair is observed registered it stays registered for
|
|
36
|
+
// the life of this process — caching it skips the shared-stream scan that
|
|
37
|
+
// register() would otherwise repeat on every workflow/child start.
|
|
38
|
+
knownRegistered = new Set();
|
|
39
|
+
constructor(journal, projections, current) {
|
|
40
|
+
this.journal = journal;
|
|
41
|
+
this.projections = projections;
|
|
42
|
+
this.current = current;
|
|
43
|
+
}
|
|
44
|
+
currentDefinition(name) {
|
|
45
|
+
const definition = this.current[name];
|
|
46
|
+
if (definition === undefined)
|
|
47
|
+
throw new Error(`Unknown workflow: ${name}`);
|
|
48
|
+
return { definition, fingerprint: workflowDefinitionFingerprint(definition) };
|
|
49
|
+
}
|
|
50
|
+
async register(name, fingerprint, definition, context) {
|
|
51
|
+
if (await this.isRegistered(name, fingerprint))
|
|
52
|
+
return;
|
|
53
|
+
const stream = workflowDefinitionsStream();
|
|
54
|
+
const draft = createEventDraft({
|
|
55
|
+
eventId: `workflow-definition:${name}:${fingerprint}`,
|
|
56
|
+
eventType: OrchestrationEventType.WorkflowDefinitionRegistered,
|
|
57
|
+
occurredAt: context.occurredAt,
|
|
58
|
+
correlationId: context.correlationId,
|
|
59
|
+
causationId: context.commandId,
|
|
60
|
+
actor: { kind: EventActorKind.System, id: 'orchestration' },
|
|
61
|
+
source: { kind: EventSourceKind.Internal, id: 'orchestration' },
|
|
62
|
+
stream,
|
|
63
|
+
payload: { workflowName: name, fingerprint, compiledDefinition: definition },
|
|
64
|
+
});
|
|
65
|
+
// The deterministic event id makes retries idempotent; CAS protects the
|
|
66
|
+
// one shared registration stream when different definitions race to start.
|
|
67
|
+
for (;;) {
|
|
68
|
+
const sequence = (await this.journal.readStream(stream)).length;
|
|
69
|
+
try {
|
|
70
|
+
await this.journal.append(stream, sequence, [draft]);
|
|
71
|
+
this.knownRegistered.add(workflowDefinitionKey(name, fingerprint));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (await this.isRegistered(name, fingerprint))
|
|
76
|
+
return;
|
|
77
|
+
if (!(error instanceof WrongExpectedSequenceError))
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async isRegistered(name, fingerprint) {
|
|
83
|
+
const key = workflowDefinitionKey(name, fingerprint);
|
|
84
|
+
if (this.knownRegistered.has(key))
|
|
85
|
+
return true;
|
|
86
|
+
const found = (await this.journal.readStream(workflowDefinitionsStream())).some((event) => {
|
|
87
|
+
const owned = selectOrchestrationEvent(event);
|
|
88
|
+
return (owned?.eventType === OrchestrationEventType.WorkflowDefinitionRegistered &&
|
|
89
|
+
owned.payload.workflowName === name &&
|
|
90
|
+
owned.payload.fingerprint === fingerprint);
|
|
91
|
+
});
|
|
92
|
+
if (found)
|
|
93
|
+
this.knownRegistered.add(key);
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
async resolve(view) {
|
|
97
|
+
if (view.workflowDefinitionFingerprint === undefined)
|
|
98
|
+
return this.currentDefinition(view.workflowName).definition;
|
|
99
|
+
const current = this.current[view.workflowName];
|
|
100
|
+
if (current !== undefined &&
|
|
101
|
+
workflowDefinitionFingerprint(current) === view.workflowDefinitionFingerprint)
|
|
102
|
+
return current;
|
|
103
|
+
const stored = await this.projections?.read(workflowDefinitionsProjection.name, workflowDefinitionKey(view.workflowName, view.workflowDefinitionFingerprint));
|
|
104
|
+
if (stored === null || stored === undefined)
|
|
105
|
+
throw new WorkflowDefinitionUnavailableError();
|
|
106
|
+
return stored.value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ApprovalAuthorityKind, WorkflowStatus } from './vocabulary.js';
|
|
3
|
+
import { PullRequestCheckState } from '../../activities/index.js';
|
|
3
4
|
import { WorkspaceMode } from '../../execution/index.js';
|
|
4
5
|
import { MatchMode } from '../../kernel/index.js';
|
|
5
6
|
const identifier = z.string().trim().min(1);
|
|
@@ -33,6 +34,9 @@ const watchGateConfigSchema = z.union([
|
|
|
33
34
|
const commandName = z.string().regex(/^\/[a-z][a-z0-9-]*$/);
|
|
34
35
|
const canonicalEventName = z.string().regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/);
|
|
35
36
|
const watchStatus = z.enum([WorkflowStatus.Active, WorkflowStatus.Waiting, WorkflowStatus.Blocked]);
|
|
37
|
+
const failingChecksWatchPredicateSchema = z
|
|
38
|
+
.object({ checks: z.literal(PullRequestCheckState.Failing) })
|
|
39
|
+
.strict();
|
|
36
40
|
export const watchConfigSchema = z
|
|
37
41
|
.object({
|
|
38
42
|
id: identifier,
|
|
@@ -46,6 +50,7 @@ export const watchConfigSchema = z
|
|
|
46
50
|
.object({ events: z.array(canonicalEventName).min(1).readonly() })
|
|
47
51
|
.strict()
|
|
48
52
|
.optional(),
|
|
53
|
+
where: failingChecksWatchPredicateSchema.optional(),
|
|
49
54
|
schedule: z.object({ cron: identifier }).strict().optional(),
|
|
50
55
|
workflow: identifier,
|
|
51
56
|
maxPerGroup: z.number().int().positive(),
|
|
@@ -29,6 +29,12 @@ export const childGroupStreamSchema = z
|
|
|
29
29
|
id: childGroupIdSchema,
|
|
30
30
|
})
|
|
31
31
|
.strict();
|
|
32
|
+
const workflowDefinitionsStreamSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
kind: z.literal(OrchestrationStreamKind.WorkflowDefinitions),
|
|
35
|
+
id: z.literal('registry'),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
32
38
|
export const childMetadataShape = {
|
|
33
39
|
parentWorkflowInstanceId: workflowInstanceIdSchema,
|
|
34
40
|
watchId: z.string().min(1),
|
|
@@ -169,6 +175,7 @@ const eventSchema = z.discriminatedUnion('eventType', [
|
|
|
169
175
|
workflowName: brandedStringSchema(workflowName),
|
|
170
176
|
orchestrationGroupId: brandedStringSchema(orchestrationGroupId),
|
|
171
177
|
entry: brandedStringSchema(stageName),
|
|
178
|
+
workflowDefinitionFingerprint: z.string().min(1).optional(),
|
|
172
179
|
})
|
|
173
180
|
.strict(),
|
|
174
181
|
z
|
|
@@ -176,10 +183,22 @@ const eventSchema = z.discriminatedUnion('eventType', [
|
|
|
176
183
|
workItemId: brandedStringSchema(workItemId),
|
|
177
184
|
workflowName: brandedStringSchema(workflowName),
|
|
178
185
|
entry: brandedStringSchema(stageName),
|
|
186
|
+
workflowDefinitionFingerprint: z.string().min(1).optional(),
|
|
179
187
|
...childMetadataShape,
|
|
180
188
|
})
|
|
181
189
|
.strict(),
|
|
182
190
|
])),
|
|
191
|
+
eventEnvelopeSchema.extend({
|
|
192
|
+
eventType: z.literal(OrchestrationEventType.WorkflowDefinitionRegistered),
|
|
193
|
+
stream: workflowDefinitionsStreamSchema,
|
|
194
|
+
payload: z
|
|
195
|
+
.object({
|
|
196
|
+
workflowName: brandedStringSchema(workflowName),
|
|
197
|
+
fingerprint: z.string().min(1),
|
|
198
|
+
compiledDefinition: z.custom(),
|
|
199
|
+
})
|
|
200
|
+
.strict(),
|
|
201
|
+
}),
|
|
183
202
|
workflowEnvelope(OrchestrationEventType.StageEntered, z.object({ stage: brandedStringSchema(stageName) }).strict()),
|
|
184
203
|
workflowEnvelope(OrchestrationEventType.ActivityRequested, activityRequestedSchema),
|
|
185
204
|
workflowEnvelope(OrchestrationEventType.ActivityStarted, z.object({ activationId: brandedStringSchema(activationId) }).strict()),
|
|
@@ -241,6 +260,8 @@ export function decodeOrchestrationEvent(event) {
|
|
|
241
260
|
const result = eventSchema.safeParse(event);
|
|
242
261
|
if (!result.success)
|
|
243
262
|
throw invalidOrchestrationEvent(event, result.error);
|
|
263
|
+
// Zod represents optional object members as `T | undefined`; the durable
|
|
264
|
+
// contract uses exact optional members for compatibility with old events.
|
|
244
265
|
return result.data;
|
|
245
266
|
}
|
|
246
267
|
export function selectOrchestrationEvent(event) {
|
|
@@ -253,6 +274,7 @@ export function selectWorkflowOrchestrationEvent(event) {
|
|
|
253
274
|
switch (owned.eventType) {
|
|
254
275
|
case OrchestrationEventType.PrimaryClaimed:
|
|
255
276
|
case OrchestrationEventType.GroupClaimed:
|
|
277
|
+
case OrchestrationEventType.WorkflowDefinitionRegistered:
|
|
256
278
|
return null;
|
|
257
279
|
default:
|
|
258
280
|
return owned;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { signalName } from './identifiers.js';
|
|
2
2
|
export const OrchestrationEventType = {
|
|
3
|
+
WorkflowDefinitionRegistered: 'orchestration.workflow-definition-registered',
|
|
3
4
|
InstanceStarted: 'orchestration.instance-started',
|
|
4
5
|
StageEntered: 'orchestration.stage-entered',
|
|
5
6
|
ActivityRequested: 'orchestration.activity-requested',
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export const OrchestrationStreamKind = {
|
|
2
2
|
WorkflowInstance: 'workflow-instance',
|
|
3
3
|
Group: 'orchestration-group',
|
|
4
|
+
WorkflowDefinitions: 'workflow-definitions',
|
|
4
5
|
};
|
|
6
|
+
export const workflowDefinitionsStream = () => ({
|
|
7
|
+
kind: OrchestrationStreamKind.WorkflowDefinitions,
|
|
8
|
+
id: 'registry',
|
|
9
|
+
});
|
|
5
10
|
export const orchestrationGroupStreamId = (value) => {
|
|
6
11
|
return value.startsWith('primary:')
|
|
7
12
|
? primaryOrchestrationGroupStreamId(value)
|
|
@@ -23,6 +23,9 @@ export function startInstance(input) {
|
|
|
23
23
|
workflowName: input.definition.name,
|
|
24
24
|
orchestrationGroupId: input.orchestrationGroupId,
|
|
25
25
|
entry: input.definition.entry,
|
|
26
|
+
...(input.workflowDefinitionFingerprint === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { workflowDefinitionFingerprint: input.workflowDefinitionFingerprint }),
|
|
26
29
|
...child,
|
|
27
30
|
}, childEvents.length + 1),
|
|
28
31
|
startDraft(input, OrchestrationEventType.StageEntered, { stage: input.definition.entry }, childEvents.length + 2),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { activityName, ActivityOutcomeKind, } from '../../activities/index.js';
|
|
1
|
+
import { ActivityEventType, activityName, ActivityOutcomeKind, } from '../../activities/index.js';
|
|
2
2
|
import { workflowDefinitionConfigSchema, } from '../contracts/config.js';
|
|
3
3
|
import { commandName, signalName, stageName, watchId, workflowName, } from '../contracts/identifiers.js';
|
|
4
4
|
import { ApprovalAuthorityKind, TransitionTargetKind } from '../contracts/vocabulary.js';
|
|
@@ -42,6 +42,10 @@ function compileWatches(config, stageNames, knownWorkflowNames) {
|
|
|
42
42
|
throw new Error(`Unknown watch stage: ${unknownStages.join(', ')}`);
|
|
43
43
|
if (!knownWorkflowNames.includes(watch.workflow))
|
|
44
44
|
throw new Error(`Unknown watch workflow: ${watch.workflow}`);
|
|
45
|
+
if (watch.where !== undefined &&
|
|
46
|
+
(watch.on === undefined ||
|
|
47
|
+
watch.on.events.some((event) => event !== ActivityEventType.PrChecksChanged)))
|
|
48
|
+
throw new Error('Watch where.checks is only valid with pr.checks-changed');
|
|
45
49
|
return Object.freeze({
|
|
46
50
|
...watch,
|
|
47
51
|
id: watchId(watch.id),
|
|
@@ -204,6 +204,9 @@ export function immutableWorkflowInstanceView(state) {
|
|
|
204
204
|
workflowInstanceId: state.workflowInstanceId,
|
|
205
205
|
workItemId: state.workItemId,
|
|
206
206
|
workflowName: state.workflowName,
|
|
207
|
+
...(state.workflowDefinitionFingerprint === undefined
|
|
208
|
+
? {}
|
|
209
|
+
: { workflowDefinitionFingerprint: state.workflowDefinitionFingerprint }),
|
|
207
210
|
orchestrationGroupId: state.orchestrationGroupId,
|
|
208
211
|
...(state.parentWorkflowInstanceId === undefined
|
|
209
212
|
? {}
|
|
@@ -10,6 +10,9 @@ export function foldWorkflowInstance(events) {
|
|
|
10
10
|
workflowInstanceId: first.stream.id,
|
|
11
11
|
workItemId: first.payload.workItemId,
|
|
12
12
|
workflowName: first.payload.workflowName,
|
|
13
|
+
...(first.payload.workflowDefinitionFingerprint === undefined
|
|
14
|
+
? {}
|
|
15
|
+
: { workflowDefinitionFingerprint: first.payload.workflowDefinitionFingerprint }),
|
|
13
16
|
orchestrationGroupId: first.payload.orchestrationGroupId,
|
|
14
17
|
...optionalChildFields(first.payload),
|
|
15
18
|
status: WorkflowStatus.Active,
|
|
@@ -4,6 +4,7 @@ export * from './application/orchestration-service.js';
|
|
|
4
4
|
export * from './application/advance-workflow.js';
|
|
5
5
|
export * from './application/signal-reactor.js';
|
|
6
6
|
export * from './application/watch-reactor.js';
|
|
7
|
+
export * from './application/workflow-definition-registry.js';
|
|
7
8
|
export * from './contracts/activity-outcome.js';
|
|
8
9
|
export * from './contracts/commands.js';
|
|
9
10
|
export * from './contracts/config.js';
|