@atolis-hq/wake 0.3.10 → 0.3.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/activities/contracts/event-schema.js +10 -2
- package/dist/src/activities/contracts/events.js +2 -0
- package/dist/src/activities/contracts/vocabulary.js +1 -0
- package/dist/src/activities/index.js +1 -0
- package/dist/src/activities/issue/complete.js +91 -0
- package/dist/src/activities/pr/projection.js +2 -0
- package/dist/src/bootstrap/composition-root.js +2 -1
- package/dist/src/bootstrap/surface-cli-applications.js +1 -2
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/control-plane/application/intake-pipeline.js +4 -3
- package/dist/src/control-plane/application/runner-pipeline.js +4 -3
- package/dist/src/integrations/delivery/application/delivery-projector.js +17 -0
- package/dist/src/integrations/delivery/application/delivery-service.js +1 -1
- package/dist/src/integrations/delivery/contracts/vocabulary.js +1 -0
- package/dist/src/integrations/fake/durable-delivery-provider.js +1 -1
- package/dist/src/integrations/fake/inbound-translator.js +4 -1
- package/dist/src/integrations/github/application/inbound-translator.js +40 -4
- package/dist/src/integrations/github/application/outbound-translator.js +2 -0
- package/dist/src/integrations/github/contracts/vocabulary.js +1 -0
- package/dist/src/integrations/github/infrastructure/client.js +16 -1
- package/dist/src/integrations/github/infrastructure/delivery.js +14 -3
- package/dist/src/integrations/github/provider.js +12 -0
- package/dist/src/resources/application/resource-projections.js +5 -0
- package/dist/src/resources/application/resource-service.js +10 -0
- package/dist/src/resources/contracts/events.js +12 -0
- package/dist/src/resources/contracts/vocabulary.js +2 -0
- package/dist/src/resources/domain/resource.js +3 -0
- package/package.json +1 -1
|
@@ -116,6 +116,14 @@ export const deniedOutcomeSchema = z
|
|
|
116
116
|
.strict();
|
|
117
117
|
export function createResourceFactDraftSchemas(eventTypes) {
|
|
118
118
|
return [
|
|
119
|
+
resourceFactDraft(eventTypes.IssueCompleteRequested, z
|
|
120
|
+
.object({
|
|
121
|
+
idempotencyKey: z.string(),
|
|
122
|
+
activationId: brandedStringSchema(activationId),
|
|
123
|
+
workflowInstanceId: z.string().min(1),
|
|
124
|
+
resourceId: resourceIdSchema,
|
|
125
|
+
})
|
|
126
|
+
.strict()),
|
|
119
127
|
resourceFactDraft(eventTypes.PrDiscovered, z
|
|
120
128
|
.object({
|
|
121
129
|
workItemId: workItemIdSchema,
|
|
@@ -210,7 +218,7 @@ function createDecisionClaimSchemas(eventTypes, resourceFacts, denials) {
|
|
|
210
218
|
activationId: brandedStringSchema(activationId),
|
|
211
219
|
decisionKind: z.literal('requested'),
|
|
212
220
|
outcome: requestedOutcomeSchema,
|
|
213
|
-
fact: resourceFacts[
|
|
221
|
+
fact: resourceFacts[10],
|
|
214
222
|
})
|
|
215
223
|
.strict(),
|
|
216
224
|
z
|
|
@@ -230,7 +238,7 @@ function createDecisionClaimSchemas(eventTypes, resourceFacts, denials) {
|
|
|
230
238
|
activationId: brandedStringSchema(activationId),
|
|
231
239
|
decisionKind: z.literal('requested'),
|
|
232
240
|
outcome: requestedOutcomeSchema,
|
|
233
|
-
fact: resourceFacts[
|
|
241
|
+
fact: resourceFacts[11],
|
|
234
242
|
})
|
|
235
243
|
.strict(),
|
|
236
244
|
z
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createActivityEventSchemas } from './event-schema.js';
|
|
2
2
|
export const ActivityEventType = {
|
|
3
|
+
IssueCompleteRequested: 'issue.complete-requested',
|
|
3
4
|
PrDiscovered: 'pr.discovered',
|
|
4
5
|
PrRevisionChanged: 'pr.revision-changed',
|
|
5
6
|
PrStateChanged: 'pr.state-changed',
|
|
@@ -34,6 +35,7 @@ export function decodeActivityEventDraft(draft) {
|
|
|
34
35
|
}
|
|
35
36
|
function ownsActivityEventType(eventType) {
|
|
36
37
|
return (eventType.startsWith('activities.') ||
|
|
38
|
+
eventType.startsWith('issue.') ||
|
|
37
39
|
eventType.startsWith('pr.') ||
|
|
38
40
|
eventType.startsWith('review.'));
|
|
39
41
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './agent/agent-activity-definition.js';
|
|
2
2
|
export * from './agent/agent-activity.js';
|
|
3
3
|
export * from './agent/agent-result.js';
|
|
4
|
+
export * from './issue/complete.js';
|
|
4
5
|
export * from './contracts/activity.js';
|
|
5
6
|
export * from './contracts/config.js';
|
|
6
7
|
export * from './contracts/events.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createEventDraft, EventActorKind, EventSourceKind, } from '../../kernel/index.js';
|
|
3
|
+
import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceStream, } from '../../resources/index.js';
|
|
4
|
+
import { ActivityEventType } from '../contracts/events.js';
|
|
5
|
+
import { ActivityExecutionKind, ActivityOutcomeKind, ActivityResourceRole, BuiltInActivityName, } from '../contracts/vocabulary.js';
|
|
6
|
+
const inputSchema = z
|
|
7
|
+
.object({ target: z.literal(ActivityResourceRole.Primary).default(ActivityResourceRole.Primary) })
|
|
8
|
+
.strict();
|
|
9
|
+
const outcomeSchema = z.union([
|
|
10
|
+
z
|
|
11
|
+
.object({
|
|
12
|
+
kind: z.literal(ActivityOutcomeKind.Done),
|
|
13
|
+
data: z.object({ deliveryEventId: z.string() }).strict(),
|
|
14
|
+
})
|
|
15
|
+
.strict(),
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
kind: z.literal(ActivityOutcomeKind.Waiting),
|
|
19
|
+
data: z
|
|
20
|
+
.object({ intentEventId: z.string(), signalKind: z.literal('delivery-result') })
|
|
21
|
+
.strict(),
|
|
22
|
+
})
|
|
23
|
+
.strict(),
|
|
24
|
+
z
|
|
25
|
+
.object({
|
|
26
|
+
kind: z.literal(ActivityOutcomeKind.Blocked),
|
|
27
|
+
data: z.object({ reason: z.literal('missing-completable-primary-issue') }).strict(),
|
|
28
|
+
})
|
|
29
|
+
.strict(),
|
|
30
|
+
]);
|
|
31
|
+
/** Requests completion of the one primary issue only; delivery owns provider effects. */
|
|
32
|
+
export function createIssueCompleteActivity(journal, resources) {
|
|
33
|
+
return {
|
|
34
|
+
name: BuiltInActivityName.IssueComplete,
|
|
35
|
+
inputSchema,
|
|
36
|
+
outcomeSchema,
|
|
37
|
+
outcomeKinds: [
|
|
38
|
+
ActivityOutcomeKind.Waiting,
|
|
39
|
+
ActivityOutcomeKind.Done,
|
|
40
|
+
ActivityOutcomeKind.Blocked,
|
|
41
|
+
],
|
|
42
|
+
resources: [],
|
|
43
|
+
executionKind: ActivityExecutionKind.Deterministic,
|
|
44
|
+
handler: {
|
|
45
|
+
execute: (invocation, context) => execute(journal, resources, invocation, context.occurredAt),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function execute(journal, resources, invocation, occurredAt) {
|
|
50
|
+
const candidates = await Promise.all(invocation.resources.map(async (candidate) => ({
|
|
51
|
+
candidate,
|
|
52
|
+
correlations: await resources.correlations(candidate.resourceId),
|
|
53
|
+
})));
|
|
54
|
+
const resource = candidates.filter(({ candidate, correlations }) => candidate.kind === BuiltInResourceKind.Issue &&
|
|
55
|
+
candidate.capabilities.includes(BuiltInResourceCapability.Completable) &&
|
|
56
|
+
correlations.some((correlation) => correlation.role === ResourceCorrelationRole.Primary &&
|
|
57
|
+
correlation.workItemId === invocation.workItemId));
|
|
58
|
+
if (resource.length !== 1)
|
|
59
|
+
return {
|
|
60
|
+
kind: ActivityOutcomeKind.Blocked,
|
|
61
|
+
data: { reason: 'missing-completable-primary-issue' },
|
|
62
|
+
};
|
|
63
|
+
const target = resource[0].candidate;
|
|
64
|
+
const eventId = `${invocation.activationId}:${ActivityEventType.IssueCompleteRequested}`;
|
|
65
|
+
const stream = resourceStream(target.resourceId);
|
|
66
|
+
const existing = await journal.readStream(stream);
|
|
67
|
+
if (!existing.some((event) => event.eventId === eventId)) {
|
|
68
|
+
await journal.append(stream, existing.length, [
|
|
69
|
+
createEventDraft({
|
|
70
|
+
eventId,
|
|
71
|
+
eventType: ActivityEventType.IssueCompleteRequested,
|
|
72
|
+
occurredAt,
|
|
73
|
+
correlationId: invocation.orchestrationGroupId,
|
|
74
|
+
causationId: invocation.activationId,
|
|
75
|
+
actor: { kind: EventActorKind.System, id: 'activities-issue' },
|
|
76
|
+
source: { kind: EventSourceKind.Internal, id: 'activities-issue' },
|
|
77
|
+
stream,
|
|
78
|
+
payload: {
|
|
79
|
+
idempotencyKey: eventId,
|
|
80
|
+
activationId: invocation.activationId,
|
|
81
|
+
workflowInstanceId: invocation.workflowInstanceId,
|
|
82
|
+
resourceId: target.resourceId,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
kind: ActivityOutcomeKind.Waiting,
|
|
89
|
+
data: { intentEventId: eventId, signalKind: 'delivery-result' },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -77,6 +77,7 @@ function projectPullRequest(previous, event) {
|
|
|
77
77
|
}
|
|
78
78
|
function ignorePullRequestEvent(previous, event) {
|
|
79
79
|
switch (event.eventType) {
|
|
80
|
+
case ActivityEventType.IssueCompleteRequested:
|
|
80
81
|
case ActivityEventType.ReviewAcceptanceSignalRecorded:
|
|
81
82
|
case ActivityEventType.PrReviewRejected:
|
|
82
83
|
case ActivityEventType.PrMergeDenied:
|
|
@@ -120,6 +121,7 @@ function ignoreAcceptedSignalFact(previous, event) {
|
|
|
120
121
|
case ActivityEventType.PrMergeAuthorized:
|
|
121
122
|
case ActivityEventType.PrApproveRequested:
|
|
122
123
|
case ActivityEventType.PrMergeRequested:
|
|
124
|
+
case ActivityEventType.IssueCompleteRequested:
|
|
123
125
|
return previous;
|
|
124
126
|
default:
|
|
125
127
|
return assertNever(event);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, createPullRequestService, } from '../activities/index.js';
|
|
1
|
+
import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createIssueCompleteActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, createPullRequestService, } from '../activities/index.js';
|
|
2
2
|
import { ControlStreamKind, DispatchPolicy, ScheduleService, createAdvanceOnce, createControlPlaneService, createIntakePipeline, createRunnerControlService, createRunnerPipeline, createWorkCancellationPolicy, ineligibleRunners, } from '../control-plane/index.js';
|
|
3
3
|
import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService, RunRepository, TranscriptStore, createExecutionService, loadPromptTemplate, renderPromptTemplate, } from '../execution/index.js';
|
|
4
4
|
import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
|
|
@@ -370,6 +370,7 @@ function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoo
|
|
|
370
370
|
}, contextReader),
|
|
371
371
|
});
|
|
372
372
|
activities.register(createStatusPublishActivity(journal));
|
|
373
|
+
activities.register(createIssueCompleteActivity(journal, resources));
|
|
373
374
|
activities.register(createPullRequestApproveActivity(journal, pullRequests));
|
|
374
375
|
activities.register(createPullRequestMergeActivity(journal, pullRequests));
|
|
375
376
|
return activities;
|
|
@@ -118,8 +118,7 @@ export async function runProjectionPump(root, signal) {
|
|
|
118
118
|
const intervalMs = 1000;
|
|
119
119
|
while (!signal.aborted) {
|
|
120
120
|
try {
|
|
121
|
-
|
|
122
|
-
await root.projectionRunner.runRegisteredOnce();
|
|
121
|
+
await root.projectionRunner.runRegisteredOnce();
|
|
123
122
|
}
|
|
124
123
|
catch (error) {
|
|
125
124
|
process.stderr.write(`Wake projection pump failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -8,8 +8,10 @@ export function createIntakePipeline(stages) {
|
|
|
8
8
|
const isPaused = async () => (await stages.isPaused?.()) ?? false;
|
|
9
9
|
return {
|
|
10
10
|
async run(signal) {
|
|
11
|
-
if (await isPaused())
|
|
11
|
+
if (await isPaused()) {
|
|
12
|
+
await stages.catchUpProjections();
|
|
12
13
|
return { processed: false };
|
|
14
|
+
}
|
|
13
15
|
await stages.catchUpProjections();
|
|
14
16
|
try {
|
|
15
17
|
if (await isPaused())
|
|
@@ -21,8 +23,7 @@ export function createIntakePipeline(stages) {
|
|
|
21
23
|
return { processed: polled > 0 || translated > 0 };
|
|
22
24
|
}
|
|
23
25
|
finally {
|
|
24
|
-
|
|
25
|
-
await stages.catchUpProjections();
|
|
26
|
+
await stages.catchUpProjections();
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
29
|
};
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
export function createRunnerPipeline(stages) {
|
|
8
8
|
const isPaused = async () => (await stages.isPaused?.()) ?? false;
|
|
9
9
|
const runOnce = async (options, signal) => {
|
|
10
|
-
if (await isPaused())
|
|
10
|
+
if (await isPaused()) {
|
|
11
|
+
await stages.catchUpProjections();
|
|
11
12
|
return { kind: 'paused' };
|
|
13
|
+
}
|
|
12
14
|
await stages.catchUpProjections();
|
|
13
15
|
try {
|
|
14
16
|
if (await isPaused())
|
|
@@ -38,8 +40,7 @@ export function createRunnerPipeline(stages) {
|
|
|
38
40
|
return result;
|
|
39
41
|
}
|
|
40
42
|
finally {
|
|
41
|
-
|
|
42
|
-
await stages.catchUpProjections();
|
|
43
|
+
await stages.catchUpProjections();
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
// The API's manual Tick Now endpoint and the resident tick host share this
|
|
@@ -45,6 +45,23 @@ function intentView(event) {
|
|
|
45
45
|
}
|
|
46
46
|
function activityIntentView(event) {
|
|
47
47
|
const intent = selectActivityEvent(event);
|
|
48
|
+
if (intent?.eventType === ActivityEventType.IssueCompleteRequested)
|
|
49
|
+
return {
|
|
50
|
+
eventId: intent.eventId,
|
|
51
|
+
view: {
|
|
52
|
+
intentEventId: intent.eventId,
|
|
53
|
+
globalPosition: intent.globalPosition,
|
|
54
|
+
workflowInstanceId: intent.payload.workflowInstanceId,
|
|
55
|
+
activationId: intent.payload.activationId,
|
|
56
|
+
kind: DeliveryIntentKind.IssueComplete,
|
|
57
|
+
resourceId: intent.payload.resourceId,
|
|
58
|
+
payload: { kind: DeliveryIntentKind.IssueComplete },
|
|
59
|
+
state: DeliveryState.Pending,
|
|
60
|
+
attempts: 0,
|
|
61
|
+
occurrenceOrdinal: 0,
|
|
62
|
+
reconciliationAttempts: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
48
65
|
if (intent?.eventType === ActivityEventType.PrApproveRequested)
|
|
49
66
|
return {
|
|
50
67
|
eventId: intent.eventId,
|
|
@@ -61,7 +61,7 @@ export class DeliveryService {
|
|
|
61
61
|
const adapter = this.dependencies.adapter(resource.adapter);
|
|
62
62
|
const occurrence = { ordinal: intent.occurrenceOrdinal + 1 };
|
|
63
63
|
if (intent.state === DeliveryState.Ambiguous || intent.attempts > 0) {
|
|
64
|
-
const reconciled = await adapter.reconcile(intent.reconciliationKey ?? intent.intentEventId, signal);
|
|
64
|
+
const reconciled = await adapter.reconcile(intent, intent.reconciliationKey ?? intent.intentEventId, signal);
|
|
65
65
|
await this.append(this.reconciled(intent, occurrence, reconciled));
|
|
66
66
|
if (reconciled.kind === DeliveryResultKind.Unknown) {
|
|
67
67
|
const count = (intent.reconciliationAttempts ?? 0) + 1;
|
|
@@ -16,6 +16,7 @@ export const DeliveryResultKind = defineClosedVocabulary({
|
|
|
16
16
|
export const DeliveryIntentKind = {
|
|
17
17
|
PrApprove: BuiltInActivityName.PullRequestApprove,
|
|
18
18
|
PrMerge: BuiltInActivityName.PullRequestMerge,
|
|
19
|
+
IssueComplete: BuiltInActivityName.IssueComplete,
|
|
19
20
|
StatusPublish: 'status.publish',
|
|
20
21
|
ReplyPublish: 'reply.publish',
|
|
21
22
|
AgentRunPublish: 'agent-run.publish',
|
|
@@ -30,7 +30,7 @@ export class DurableFakeDeliveryProvider {
|
|
|
30
30
|
rememberAmbiguous(intentEventId, reconciliationKey) {
|
|
31
31
|
this.ambiguous.set(reconciliationKey, intentEventId);
|
|
32
32
|
}
|
|
33
|
-
async reconcile(reconciliationKey) {
|
|
33
|
+
async reconcile(_intent, reconciliationKey) {
|
|
34
34
|
const intentEventId = this.ambiguous.get(reconciliationKey) ?? reconciliationKey;
|
|
35
35
|
const externalId = this.effects.get(intentEventId);
|
|
36
36
|
return externalId === undefined
|
|
@@ -73,7 +73,7 @@ export class FakeInboundTranslator {
|
|
|
73
73
|
? []
|
|
74
74
|
: [BuiltInResourceCapability.ChangedFiles]),
|
|
75
75
|
]
|
|
76
|
-
: [BuiltInResourceCapability.Commentable],
|
|
76
|
+
: [BuiltInResourceCapability.Commentable, BuiltInResourceCapability.Completable],
|
|
77
77
|
objective: evidence.title,
|
|
78
78
|
tags: evidence.tags ?? [],
|
|
79
79
|
...(evidence.revision === undefined ? {} : { revision: evidence.revision }),
|
|
@@ -100,6 +100,9 @@ export class FakeInboundTranslator {
|
|
|
100
100
|
}, context);
|
|
101
101
|
if (isPullRequest)
|
|
102
102
|
await this.observePullRequest(existing.resourceId, correlation.workItemId, evidence, event, context);
|
|
103
|
+
// Fake issue evidence has no terminal state, so its existing behaviour is
|
|
104
|
+
// intentionally unchanged; production inbound suppression lives with the
|
|
105
|
+
// provider observation that carries the terminal outcome.
|
|
103
106
|
return true;
|
|
104
107
|
}
|
|
105
108
|
async observePullRequest(resourceId, workItemId, evidence, event, context) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
|
-
import { createPullRequestService, } from '../../../activities/index.js';
|
|
2
|
+
import { ActivityEventType, createPullRequestService, } from '../../../activities/index.js';
|
|
3
3
|
import { UlidIdGenerator, } from '../../../kernel/index.js';
|
|
4
|
-
import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceId, } from '../../../resources/index.js';
|
|
4
|
+
import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, ResourceEventType, resourceId, resourceStream, } from '../../../resources/index.js';
|
|
5
5
|
import { workItemId } from '../../../work/index.js';
|
|
6
6
|
import { admitObservedWork } from '../../application/work-admission.js';
|
|
7
7
|
import { concludeObservedWork } from '../../application/work-conclusion.js';
|
|
8
8
|
import { evaluateIntakeRules } from '../../contracts/intake-rules.js';
|
|
9
|
+
import { deliveryStream } from '../../contracts/streams.js';
|
|
10
|
+
import { DeliveryEventType, selectDeliveryEvent } from '../../delivery/contracts/events.js';
|
|
9
11
|
import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
|
|
10
12
|
import { GitHubAdapter } from '../contracts/vocabulary.js';
|
|
11
13
|
import { commandContext } from './inbound-context.js';
|
|
@@ -140,7 +142,7 @@ export class InboundTranslator {
|
|
|
140
142
|
BuiltInResourceCapability.Revisioned,
|
|
141
143
|
BuiltInResourceCapability.ChangedFiles,
|
|
142
144
|
]
|
|
143
|
-
: [BuiltInResourceCapability.Commentable],
|
|
145
|
+
: [BuiltInResourceCapability.Commentable, BuiltInResourceCapability.Completable],
|
|
144
146
|
objective: payload.title,
|
|
145
147
|
tags: intake.tags,
|
|
146
148
|
revision: payload.revision,
|
|
@@ -165,7 +167,14 @@ export class InboundTranslator {
|
|
|
165
167
|
revision: payload.revision,
|
|
166
168
|
...(current.title === undefined ? {} : { title: current.title }),
|
|
167
169
|
}, context);
|
|
168
|
-
|
|
170
|
+
const wakeCompletion = await this.unconsumedWakeCompletion(resourceIdValue);
|
|
171
|
+
if (payload.outcome === undefined && wakeCompletion !== null) {
|
|
172
|
+
await this.supersedeWakeCompletion(resourceIdValue, wakeCompletion, context);
|
|
173
|
+
}
|
|
174
|
+
else if (payload.outcome !== undefined && wakeCompletion !== null) {
|
|
175
|
+
await this.consumeWakeCompletion(resourceIdValue, wakeCompletion, context);
|
|
176
|
+
}
|
|
177
|
+
else if (payload.outcome !== undefined && this.conclusion !== undefined) {
|
|
169
178
|
await concludeObservedWork({ work: this.work, conclusion: this.conclusion }, {
|
|
170
179
|
workItemId: workItemIdValue,
|
|
171
180
|
outcome: payload.outcome,
|
|
@@ -176,6 +185,33 @@ export class InboundTranslator {
|
|
|
176
185
|
if (payload.kind === 'pull-request')
|
|
177
186
|
await pullRequests.observe(observePullRequest(current.resourceId, workItemIdValue, payload), context);
|
|
178
187
|
}
|
|
188
|
+
/** A confirmed completion consumes exactly one matching terminal observation. */
|
|
189
|
+
async unconsumedWakeCompletion(resourceIdValue) {
|
|
190
|
+
const events = await this.journal.readStream(resourceStream(resourceIdValue));
|
|
191
|
+
const intents = events.filter((event) => event.eventType === ActivityEventType.IssueCompleteRequested);
|
|
192
|
+
for (const intent of intents) {
|
|
193
|
+
if (events.some((event) => (event.eventType === ResourceEventType.IssueCompletionObservationConsumed ||
|
|
194
|
+
event.eventType === ResourceEventType.IssueCompletionObservationSuperseded) &&
|
|
195
|
+
event.payload !== null &&
|
|
196
|
+
typeof event.payload === 'object' &&
|
|
197
|
+
'intentEventId' in event.payload &&
|
|
198
|
+
event.payload.intentEventId === intent.eventId))
|
|
199
|
+
continue;
|
|
200
|
+
const deliveries = await this.journal.readStream(deliveryStream(intent.eventId));
|
|
201
|
+
if (deliveries.some((event) => {
|
|
202
|
+
const delivery = selectDeliveryEvent(event);
|
|
203
|
+
return delivery?.eventType === DeliveryEventType.Confirmed;
|
|
204
|
+
}))
|
|
205
|
+
return intent.eventId;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
async consumeWakeCompletion(resourceIdValue, intentEventId, context) {
|
|
210
|
+
await this.resources.consumeIssueCompletion(resourceIdValue, intentEventId, context);
|
|
211
|
+
}
|
|
212
|
+
async supersedeWakeCompletion(resourceIdValue, intentEventId, context) {
|
|
213
|
+
await this.resources.supersedeIssueCompletion(resourceIdValue, intentEventId, context);
|
|
214
|
+
}
|
|
179
215
|
mintIdentity(externalKey) {
|
|
180
216
|
const key = `${externalKey.adapter}:${externalKey.key}`;
|
|
181
217
|
const existing = this.minted.get(key);
|
|
@@ -37,6 +37,8 @@ function outboundAction(intent) {
|
|
|
37
37
|
return 'autoMerge' in intent.payload && intent.payload.autoMerge
|
|
38
38
|
? GitHubOutboundAction.EnableAutoMerge
|
|
39
39
|
: GitHubOutboundAction.Merge;
|
|
40
|
+
case DeliveryIntentKind.IssueComplete:
|
|
41
|
+
return GitHubOutboundAction.Close;
|
|
40
42
|
case DeliveryIntentKind.StatusPublish:
|
|
41
43
|
return GitHubOutboundAction.Status;
|
|
42
44
|
case DeliveryIntentKind.ReplyPublish:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Octokit } from '@octokit/rest';
|
|
2
|
-
import { MergeMethod } from '../../../activities/index.js';
|
|
2
|
+
import { MergeMethod, PullRequestState } from '../../../activities/index.js';
|
|
3
3
|
import { GitHubOutboundAction } from '../contracts/vocabulary.js';
|
|
4
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';
|
|
@@ -39,6 +39,10 @@ export function createGitHubClient(token) {
|
|
|
39
39
|
listIssueComments: (owner, repo, issueNumber, pageSize) => listIssueComments(octokit, cache, owner, repo, issueNumber, pageSize),
|
|
40
40
|
listReviewComments: (owner, repo, pullNumber, pageSize) => listReviewComments(octokit, cache, owner, repo, pullNumber, pageSize),
|
|
41
41
|
getIssueLabels: (owner, repo, issueNumber) => getIssueLabels(octokit, cache, owner, repo, issueNumber),
|
|
42
|
+
getIssue: async (owner, repo, issueNumber) => octokit.rest.issues.get({ owner, repo, issue_number: issueNumber }).then(({ data }) => ({
|
|
43
|
+
id: String(data.id),
|
|
44
|
+
state: data.state,
|
|
45
|
+
})),
|
|
42
46
|
setIssueLabels: async (owner, repo, issueNumber, labels) => {
|
|
43
47
|
await octokit.rest.issues.setLabels({
|
|
44
48
|
owner,
|
|
@@ -78,6 +82,17 @@ async function deliver(octokit, command) {
|
|
|
78
82
|
if (command.action === GitHubOutboundAction.EnableAutoMerge) {
|
|
79
83
|
return enableAutoMerge(octokit, command);
|
|
80
84
|
}
|
|
85
|
+
if (command.action === GitHubOutboundAction.Close) {
|
|
86
|
+
if (command.issue_number === undefined)
|
|
87
|
+
throw new Error('GitHub issue completion requires an issue');
|
|
88
|
+
const response = await octokit.rest.issues.update({
|
|
89
|
+
owner: command.owner,
|
|
90
|
+
repo: command.repo,
|
|
91
|
+
issue_number: command.issue_number,
|
|
92
|
+
state: PullRequestState.Closed,
|
|
93
|
+
});
|
|
94
|
+
return String(response.data.id);
|
|
95
|
+
}
|
|
81
96
|
const issueNumber = command.issue_number ?? command.pull_number;
|
|
82
97
|
if (issueNumber === undefined)
|
|
83
98
|
throw new Error('GitHub comment requires an issue or pull request');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { BuiltInActivityName } from '../../../activities/index.js';
|
|
1
2
|
import { DeliveryResultKind } from '../../delivery/contracts/vocabulary.js';
|
|
2
3
|
const GitHubDeliveryFailureCode = 'github-error';
|
|
3
|
-
export function createGitHubDelivery(deliver) {
|
|
4
|
+
export function createGitHubDelivery(deliver, reconcileIssue) {
|
|
4
5
|
return {
|
|
5
6
|
async deliver(intent) {
|
|
6
7
|
try {
|
|
@@ -17,8 +18,18 @@ export function createGitHubDelivery(deliver) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
},
|
|
20
|
-
async reconcile() {
|
|
21
|
-
|
|
21
|
+
async reconcile(intent) {
|
|
22
|
+
if (intent.kind !== BuiltInActivityName.IssueComplete || reconcileIssue === undefined)
|
|
23
|
+
return { kind: DeliveryResultKind.Unknown };
|
|
24
|
+
try {
|
|
25
|
+
const externalId = await reconcileIssue(intent);
|
|
26
|
+
return externalId === null
|
|
27
|
+
? { kind: DeliveryResultKind.NotFound }
|
|
28
|
+
: { kind: DeliveryResultKind.Confirmed, externalId };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { kind: DeliveryResultKind.Unknown };
|
|
32
|
+
}
|
|
22
33
|
},
|
|
23
34
|
};
|
|
24
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BuiltInActivityName, PullRequestState } from '../../activities/index.js';
|
|
1
2
|
import { BuiltInResourceCapability, resourceId, resourceKind } from '../../resources/index.js';
|
|
2
3
|
import { ArtifactVerificationResult } from '../contracts/artifact-vocabulary.js';
|
|
3
4
|
import { InboundTranslator } from './application/inbound-translator.js';
|
|
@@ -35,6 +36,17 @@ export const gitHubProviderDefinition = {
|
|
|
35
36
|
if (resource === null)
|
|
36
37
|
throw new Error(`GitHub resource ${intent.resourceId} is unavailable`);
|
|
37
38
|
return client.deliver({ ...translateGitHubOutbound(resource, intent), idempotencyKey });
|
|
39
|
+
}, async (intent) => {
|
|
40
|
+
if (intent.kind !== BuiltInActivityName.IssueComplete)
|
|
41
|
+
return null;
|
|
42
|
+
const resource = await services.resources.get(resourceId(intent.resourceId));
|
|
43
|
+
if (resource === null)
|
|
44
|
+
return null;
|
|
45
|
+
const parsed = parsePullRequestKey(resource.externalKey.key);
|
|
46
|
+
if (parsed === null)
|
|
47
|
+
return null;
|
|
48
|
+
const issue = await client.getIssue(parsed.owner, parsed.repo, parsed.number);
|
|
49
|
+
return issue.state === PullRequestState.Closed ? issue.id : null;
|
|
38
50
|
}),
|
|
39
51
|
verifyArtifact: async (kind, externalKey, context) => {
|
|
40
52
|
if (kind !== resourceKind('pull-request'))
|
|
@@ -6,6 +6,7 @@ export const resourceProjection = {
|
|
|
6
6
|
return owned === null ? null : { key: owned.stream.id };
|
|
7
7
|
},
|
|
8
8
|
initial: () => null,
|
|
9
|
+
// eslint-disable-next-line complexity
|
|
9
10
|
project(previous, event) {
|
|
10
11
|
const owned = selectResourceEvent(event);
|
|
11
12
|
if (owned === null)
|
|
@@ -35,6 +36,8 @@ export const resourceProjection = {
|
|
|
35
36
|
};
|
|
36
37
|
case ResourceEventType.WorkCorrelationEstablished:
|
|
37
38
|
case ResourceEventType.WorkCorrelationRetracted:
|
|
39
|
+
case ResourceEventType.IssueCompletionObservationConsumed:
|
|
40
|
+
case ResourceEventType.IssueCompletionObservationSuperseded:
|
|
38
41
|
return previous;
|
|
39
42
|
default:
|
|
40
43
|
return assertNever(owned);
|
|
@@ -70,6 +73,8 @@ export const resourceCorrelationProjection = {
|
|
|
70
73
|
case ResourceEventType.ResourceDiscovered:
|
|
71
74
|
case ResourceEventType.ResourceRevisionObserved:
|
|
72
75
|
case ResourceEventType.WorkCorrelationConflicted:
|
|
76
|
+
case ResourceEventType.IssueCompletionObservationConsumed:
|
|
77
|
+
case ResourceEventType.IssueCompletionObservationSuperseded:
|
|
73
78
|
return previous;
|
|
74
79
|
default:
|
|
75
80
|
return assertNever(owned);
|
|
@@ -24,6 +24,16 @@ export function createResourceService(journal, lookup) {
|
|
|
24
24
|
workItemId,
|
|
25
25
|
}));
|
|
26
26
|
},
|
|
27
|
+
async consumeIssueCompletion(resourceId, intentEventId, context) {
|
|
28
|
+
await appendResourceEvent(repository, resourceId, resourceDraft(resourceId, context, ResourceEventType.IssueCompletionObservationConsumed, {
|
|
29
|
+
intentEventId,
|
|
30
|
+
}));
|
|
31
|
+
},
|
|
32
|
+
async supersedeIssueCompletion(resourceId, intentEventId, context) {
|
|
33
|
+
await appendResourceEvent(repository, resourceId, resourceDraft(resourceId, context, ResourceEventType.IssueCompletionObservationSuperseded, {
|
|
34
|
+
intentEventId,
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
27
37
|
};
|
|
28
38
|
}
|
|
29
39
|
async function discoverResource(repository, command, context) {
|
|
@@ -10,6 +10,8 @@ export const ResourceEventType = {
|
|
|
10
10
|
WorkCorrelationEstablished: 'resources.work-correlation-established',
|
|
11
11
|
WorkCorrelationRetracted: 'resources.work-correlation-retracted',
|
|
12
12
|
WorkCorrelationConflicted: 'resources.work-correlation-conflicted',
|
|
13
|
+
IssueCompletionObservationConsumed: 'resources.issue-completion-observation-consumed',
|
|
14
|
+
IssueCompletionObservationSuperseded: 'resources.issue-completion-observation-superseded',
|
|
13
15
|
};
|
|
14
16
|
const streamSchema = z
|
|
15
17
|
.object({
|
|
@@ -68,6 +70,16 @@ const eventSchema = z.discriminatedUnion('eventType', [
|
|
|
68
70
|
})
|
|
69
71
|
.strict(),
|
|
70
72
|
}),
|
|
73
|
+
eventEnvelopeSchema.extend({
|
|
74
|
+
eventType: z.literal(ResourceEventType.IssueCompletionObservationConsumed),
|
|
75
|
+
stream: streamSchema,
|
|
76
|
+
payload: z.object({ intentEventId: z.string().min(1) }).strict(),
|
|
77
|
+
}),
|
|
78
|
+
eventEnvelopeSchema.extend({
|
|
79
|
+
eventType: z.literal(ResourceEventType.IssueCompletionObservationSuperseded),
|
|
80
|
+
stream: streamSchema,
|
|
81
|
+
payload: z.object({ intentEventId: z.string().min(1) }).strict(),
|
|
82
|
+
}),
|
|
71
83
|
]);
|
|
72
84
|
export function decodeResourceEvent(event) {
|
|
73
85
|
const result = eventSchema.safeParse(event);
|
|
@@ -12,6 +12,8 @@ export const BuiltInResourceCapability = {
|
|
|
12
12
|
Mergeable: resourceCapability('mergeable'),
|
|
13
13
|
Revisioned: resourceCapability('revisioned'),
|
|
14
14
|
Editable: resourceCapability('editable'),
|
|
15
|
+
/** Provider can move an issue to its completed terminal state. */
|
|
16
|
+
Completable: resourceCapability('completable'),
|
|
15
17
|
// Provider can report which file paths a revision changed.
|
|
16
18
|
ChangedFiles: resourceCapability('changed-files'),
|
|
17
19
|
};
|
|
@@ -49,6 +49,9 @@ function applyResourceEvent(resource, active, event) {
|
|
|
49
49
|
case ResourceEventType.WorkCorrelationRetracted:
|
|
50
50
|
active.delete(event.payload.workItemId);
|
|
51
51
|
break;
|
|
52
|
+
case ResourceEventType.IssueCompletionObservationConsumed:
|
|
53
|
+
case ResourceEventType.IssueCompletionObservationSuperseded:
|
|
54
|
+
break;
|
|
52
55
|
default:
|
|
53
56
|
assertNever(event);
|
|
54
57
|
}
|