@atolis-hq/wake 0.2.92 → 0.2.94
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.
|
@@ -194,7 +194,7 @@ export function createTickRunner(deps) {
|
|
|
194
194
|
isRunningRecordActive,
|
|
195
195
|
deliverOutboundEvent,
|
|
196
196
|
});
|
|
197
|
-
const { cleanupClosedIssueWorkspaces } = createWorkspaceCleanup({
|
|
197
|
+
const { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs } = createWorkspaceCleanup({
|
|
198
198
|
clock: deps.clock,
|
|
199
199
|
config: deps.config,
|
|
200
200
|
stateStore: deps.stateStore,
|
|
@@ -374,14 +374,12 @@ export function createTickRunner(deps) {
|
|
|
374
374
|
const targetResourceUri = input.approvalResolution.targetResourceUri;
|
|
375
375
|
const commentId = input.approvalResolution.triggeringCommentId.replace(/[^a-z0-9]+/gi, '-');
|
|
376
376
|
const reviewBody = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// failing doesn't stop the other from being attempted.
|
|
384
|
-
const blockedReasons = [];
|
|
377
|
+
// approve and autoMerge are independent: one failing doesn't stop the
|
|
378
|
+
// other from being attempted. If autoMerge succeeds, a failed approval is
|
|
379
|
+
// not a policy block on its own; GitHub can reject self-approval while
|
|
380
|
+
// still accepting auto-merge for repos that do not require review.
|
|
381
|
+
const approvalBlockedReasons = [];
|
|
382
|
+
const autoMergeBlockedReasons = [];
|
|
385
383
|
const approvedEventId = `pr-merge-approved-${commentId}`;
|
|
386
384
|
if (input.mergePolicy.approve &&
|
|
387
385
|
(await deps.stateStore.readEventEnvelope(approvedEventId)) === null) {
|
|
@@ -408,10 +406,11 @@ export function createTickRunner(deps) {
|
|
|
408
406
|
}));
|
|
409
407
|
}
|
|
410
408
|
catch (error) {
|
|
411
|
-
|
|
409
|
+
approvalBlockedReasons.push(`Merge policy blocked the approval step: ${describeMergeActorError(error)}`);
|
|
412
410
|
}
|
|
413
411
|
}
|
|
414
412
|
const autoMergeEventId = `pr-auto-merge-enabled-${commentId}`;
|
|
413
|
+
let autoMergeSucceeded = false;
|
|
415
414
|
if (input.mergePolicy.autoMerge &&
|
|
416
415
|
(await deps.stateStore.readEventEnvelope(autoMergeEventId)) === null) {
|
|
417
416
|
try {
|
|
@@ -435,14 +434,52 @@ export function createTickRunner(deps) {
|
|
|
435
434
|
idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-auto-merge`,
|
|
436
435
|
},
|
|
437
436
|
}));
|
|
437
|
+
autoMergeSucceeded = true;
|
|
438
438
|
}
|
|
439
439
|
catch (error) {
|
|
440
|
-
|
|
440
|
+
autoMergeBlockedReasons.push(`Merge policy blocked the auto-merge step: ${describeMergeActorError(error)}`);
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
|
+
else if (input.mergePolicy.autoMerge) {
|
|
444
|
+
autoMergeSucceeded = true;
|
|
445
|
+
}
|
|
446
|
+
const blockedReasons = autoMergeSucceeded && input.mergePolicy.autoMerge
|
|
447
|
+
? autoMergeBlockedReasons
|
|
448
|
+
: [...approvalBlockedReasons, ...autoMergeBlockedReasons];
|
|
443
449
|
if (blockedReasons.length > 0) {
|
|
444
450
|
return { blocked: true, reason: blockedReasons.join(' ') };
|
|
445
451
|
}
|
|
452
|
+
// Approval failed but auto-merge covered it (e.g. GitHub rejecting
|
|
453
|
+
// self-approval on the bot's own PR) - not a policy block, but the
|
|
454
|
+
// failure should still leave a visible trail instead of vanishing
|
|
455
|
+
// silently, since it may also be a real permissions/config problem.
|
|
456
|
+
if (approvalBlockedReasons.length > 0) {
|
|
457
|
+
const occurredAt = eventStampNow();
|
|
458
|
+
await deliverOutboundEvent(createEventEnvelope({
|
|
459
|
+
eventId: `pr-merge-approval-note-${commentId}`,
|
|
460
|
+
workItemKey: input.projection.workItemKey,
|
|
461
|
+
streamScope: 'work-item',
|
|
462
|
+
direction: 'outbound',
|
|
463
|
+
sourceSystem: 'wake',
|
|
464
|
+
sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
|
|
465
|
+
sourceRefs: {
|
|
466
|
+
repo: input.projection.issue.repo,
|
|
467
|
+
issueNumber: input.projection.issue.number,
|
|
468
|
+
resourceUri: targetResourceUri,
|
|
469
|
+
},
|
|
470
|
+
occurredAt,
|
|
471
|
+
ingestedAt: occurredAt,
|
|
472
|
+
trigger: 'context-only',
|
|
473
|
+
payload: {
|
|
474
|
+
kind: 'status-update',
|
|
475
|
+
origin: input.projection.origin ?? 'github',
|
|
476
|
+
body: `Approval step did not block the merge, but did not succeed on its own: ${approvalBlockedReasons.join(' ')}`,
|
|
477
|
+
idempotencyKey: `${commentId}:pr-merge-approval-note`,
|
|
478
|
+
deliveryState: 'PENDING',
|
|
479
|
+
},
|
|
480
|
+
derivedHints: { stage: input.projection.wake.stage },
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
446
483
|
return { blocked: false };
|
|
447
484
|
}
|
|
448
485
|
// The one deterministic approval transition: /approved, wake:auto, and a
|
|
@@ -883,6 +920,7 @@ export function createTickRunner(deps) {
|
|
|
883
920
|
}
|
|
884
921
|
const projections = await deps.stateStore.listIssueStates();
|
|
885
922
|
await cleanupClosedIssueWorkspaces(projections);
|
|
923
|
+
await sweepExpiredTranscriptDirs();
|
|
886
924
|
// Runs every tick, not only when this tick happened to poll a fresh
|
|
887
925
|
// inbound event — a stale label on a parked item (nothing else ever
|
|
888
926
|
// re-checks it once there's no next action) otherwise never self-heals.
|
|
@@ -2054,7 +2092,7 @@ export function createTickRunner(deps) {
|
|
|
2054
2092
|
},
|
|
2055
2093
|
payload: {
|
|
2056
2094
|
...publishIntent.payload,
|
|
2057
|
-
kind:
|
|
2095
|
+
kind: 'status-update',
|
|
2058
2096
|
body: sentinel === 'DONE'
|
|
2059
2097
|
? `${parsedRunnerResult.body}\n\n${prReviewApprovalMarker}`
|
|
2060
2098
|
: `${parsedRunnerResult.body}\n\n<!-- wake:pr-review-changes-requested -->`,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { rm } from 'node:fs/promises';
|
|
1
|
+
import { readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, join, relative } from 'node:path';
|
|
3
3
|
import { WORKSPACE_CLEANED_EVENT, WORKSPACE_CLEANUP_FAILED_EVENT } from '../domain/event-types.js';
|
|
4
4
|
import { createEventEnvelope } from '../lib/event-log.js';
|
|
5
|
-
|
|
5
|
+
const TRANSCRIPT_CLEANED_AT_MARKER = '.cleaned-at';
|
|
6
|
+
// Cleans up per-issue workspaces and applies transcript retention once the
|
|
6
7
|
// originating issue is closed. A cleanup failure is recorded as an event and
|
|
7
8
|
// skipped rather than aborting the sweep.
|
|
8
9
|
export function createWorkspaceCleanup(deps) {
|
|
@@ -14,6 +15,49 @@ export function createWorkspaceCleanup(deps) {
|
|
|
14
15
|
const rel = relative(workspacesRoot, workspacePath);
|
|
15
16
|
return !rel.startsWith('..') && !isAbsolute(rel) && rel.length > 0;
|
|
16
17
|
}
|
|
18
|
+
async function markTranscriptDirectoryCleaned(workItemKey, cleanedAt) {
|
|
19
|
+
const transcriptDir = deps.stateStore.paths.transcriptWorkDir(workItemKey);
|
|
20
|
+
const transcriptDirStat = await stat(transcriptDir).catch(() => undefined);
|
|
21
|
+
if (transcriptDirStat === undefined || !transcriptDirStat.isDirectory()) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await writeFile(join(transcriptDir, TRANSCRIPT_CLEANED_AT_MARKER), `${cleanedAt}\n`, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
async function applyTranscriptCleanupRetention(workItemKey, cleanedAt) {
|
|
27
|
+
if (deps.config.transcripts.retentionMs === 0) {
|
|
28
|
+
await rm(deps.stateStore.paths.transcriptWorkDir(workItemKey), {
|
|
29
|
+
recursive: true,
|
|
30
|
+
force: true,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await markTranscriptDirectoryCleaned(workItemKey, cleanedAt);
|
|
35
|
+
}
|
|
36
|
+
async function sweepExpiredTranscriptDirs() {
|
|
37
|
+
const retentionMs = deps.config.transcripts.retentionMs;
|
|
38
|
+
const transcriptDirs = await readdir(deps.stateStore.paths.transcriptsRoot, {
|
|
39
|
+
withFileTypes: true,
|
|
40
|
+
}).catch(() => []);
|
|
41
|
+
const nowMs = deps.clock.now().getTime();
|
|
42
|
+
for (const entry of transcriptDirs) {
|
|
43
|
+
if (!entry.isDirectory()) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const transcriptDir = join(deps.stateStore.paths.transcriptsRoot, entry.name);
|
|
47
|
+
const markerPath = join(transcriptDir, TRANSCRIPT_CLEANED_AT_MARKER);
|
|
48
|
+
const marker = await readFile(markerPath, 'utf8').catch(() => undefined);
|
|
49
|
+
if (marker === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const cleanedAtMs = Date.parse(marker.trim());
|
|
53
|
+
if (!Number.isFinite(cleanedAtMs)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (nowMs - cleanedAtMs >= retentionMs) {
|
|
57
|
+
await rm(transcriptDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
17
61
|
async function cleanupClosedIssueWorkspaces(projections) {
|
|
18
62
|
for (const projection of projections) {
|
|
19
63
|
const { workspacePath } = projection.wake;
|
|
@@ -22,12 +66,7 @@ export function createWorkspaceCleanup(deps) {
|
|
|
22
66
|
isPerIssueWorkspacePath(workspacePath)) {
|
|
23
67
|
try {
|
|
24
68
|
await deps.workspaceManager.cleanupWorkspace({ workspacePath });
|
|
25
|
-
|
|
26
|
-
await rm(deps.stateStore.paths.transcriptWorkDir(projection.workItemKey), {
|
|
27
|
-
recursive: true,
|
|
28
|
-
force: true,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
69
|
+
await applyTranscriptCleanupRetention(projection.workItemKey, eventStampNow());
|
|
31
70
|
}
|
|
32
71
|
catch (error) {
|
|
33
72
|
const failedAt = eventStampNow();
|
|
@@ -74,5 +113,5 @@ export function createWorkspaceCleanup(deps) {
|
|
|
74
113
|
}
|
|
75
114
|
}
|
|
76
115
|
}
|
|
77
|
-
return { cleanupClosedIssueWorkspaces };
|
|
116
|
+
return { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs };
|
|
78
117
|
}
|
|
@@ -691,9 +691,13 @@ const wakeConfigBaseSchema = z.object({
|
|
|
691
691
|
transcripts: z
|
|
692
692
|
.object({
|
|
693
693
|
enabled: z.boolean().default(false),
|
|
694
|
-
|
|
694
|
+
retentionMs: z
|
|
695
|
+
.number()
|
|
696
|
+
.int()
|
|
697
|
+
.nonnegative()
|
|
698
|
+
.default(3 * 24 * 60 * 60 * 1000),
|
|
695
699
|
})
|
|
696
|
-
.default({ enabled: false,
|
|
700
|
+
.default({ enabled: false, retentionMs: 3 * 24 * 60 * 60 * 1000 }),
|
|
697
701
|
retry: z
|
|
698
702
|
.object({
|
|
699
703
|
maxFailureRetries: z.number().int().positive().default(5),
|
|
@@ -1090,8 +1094,16 @@ function attachDerivedPromptContext(config) {
|
|
|
1090
1094
|
return config;
|
|
1091
1095
|
}
|
|
1092
1096
|
export function parseWakeConfig(input) {
|
|
1097
|
+
if (isRecord(input) &&
|
|
1098
|
+
isRecord(input.transcripts) &&
|
|
1099
|
+
'retainAfterWorkspaceCleanup' in input.transcripts) {
|
|
1100
|
+
throw new Error('transcripts.retainAfterWorkspaceCleanup is no longer supported; use transcripts.retentionMs instead. Set transcripts.retentionMs to 0 to delete transcripts immediately on workspace cleanup.');
|
|
1101
|
+
}
|
|
1093
1102
|
return structuredClone(attachDerivedPromptContext(wakeConfigSchema.parse(input)));
|
|
1094
1103
|
}
|
|
1104
|
+
function isRecord(value) {
|
|
1105
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1106
|
+
}
|
|
1095
1107
|
export function parseSourceStateRecord(input) {
|
|
1096
1108
|
return sourceStateRecordSchema.parse(input);
|
|
1097
1109
|
}
|
package/dist/src/version.js
CHANGED