@atolis-hq/wake 0.3.75 → 0.3.76
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/bootstrap/surface-api-applications.js +10 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/application/adapter-health-tracker.js +43 -0
- package/dist/src/integrations/contracts/provider.js +8 -5
- package/dist/src/integrations/github/infrastructure/adapter-health-registry.js +0 -0
- package/dist/src/integrations/github/infrastructure/source.js +17 -5
- package/dist/src/integrations/github/provider.js +22 -6
- package/dist/src/surfaces/web-assets/assets/{index-DHGUQVg7.js → index-CBE1yusZ.js} +1 -1
- package/dist/src/surfaces/web-assets/index.html +1 -1
- package/package.json +1 -1
|
@@ -203,12 +203,21 @@ function createSystemApplications(root, now) {
|
|
|
203
203
|
detail: `rolled back from ${selfUpdateFailure.tag} at ${selfUpdateFailure.occurredAt}: ${selfUpdateFailure.message}`,
|
|
204
204
|
},
|
|
205
205
|
];
|
|
206
|
+
const adapters = root.providers.flatMap((instance) => (instance.health?.() ?? []).map((check) => ({
|
|
207
|
+
adapter: instance.adapter,
|
|
208
|
+
provider: instance.provider,
|
|
209
|
+
...check,
|
|
210
|
+
})));
|
|
206
211
|
return {
|
|
207
212
|
data: {
|
|
208
|
-
status: checks.some((check) => check.status === 'degraded')
|
|
213
|
+
status: checks.some((check) => check.status === 'degraded') ||
|
|
214
|
+
adapters.some((check) => check.status === 'degraded')
|
|
215
|
+
? 'degraded'
|
|
216
|
+
: 'ok',
|
|
209
217
|
version: wakeVersion,
|
|
210
218
|
checkedAt,
|
|
211
219
|
checks,
|
|
220
|
+
adapters,
|
|
212
221
|
},
|
|
213
222
|
meta: sampledMeta(checkedAt),
|
|
214
223
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const DEFINITIVE_STATUSES = new Set([401, 403, 429]);
|
|
2
|
+
export function createAdapterHealthTracker(options = {}) {
|
|
3
|
+
const now = options.now ?? Date.now;
|
|
4
|
+
const threshold = options.degradeAfterConsecutiveFailures ?? 3;
|
|
5
|
+
let successCount = 0;
|
|
6
|
+
let failureCount = 0;
|
|
7
|
+
let consecutiveFailures = 0;
|
|
8
|
+
let degradedDetail;
|
|
9
|
+
return {
|
|
10
|
+
recordSuccess() {
|
|
11
|
+
successCount += 1;
|
|
12
|
+
consecutiveFailures = 0;
|
|
13
|
+
degradedDetail = undefined;
|
|
14
|
+
},
|
|
15
|
+
recordFailure(error) {
|
|
16
|
+
failureCount += 1;
|
|
17
|
+
consecutiveFailures += 1;
|
|
18
|
+
const status = statusOf(error);
|
|
19
|
+
const occurredAt = new Date(now()).toISOString();
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
if (status !== undefined && DEFINITIVE_STATUSES.has(status)) {
|
|
22
|
+
degradedDetail = `${status} at ${occurredAt}: ${message}`;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (consecutiveFailures >= threshold) {
|
|
26
|
+
degradedDetail = `${consecutiveFailures} consecutive failures, last: ${status ?? 'error'} at ${occurredAt}: ${message}`;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
snapshot() {
|
|
30
|
+
return {
|
|
31
|
+
status: degradedDetail === undefined ? 'ok' : 'degraded',
|
|
32
|
+
...(degradedDetail === undefined ? {} : { detail: degradedDetail }),
|
|
33
|
+
successCount,
|
|
34
|
+
failureCount,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function statusOf(error) {
|
|
40
|
+
if (typeof error !== 'object' || error === null || !('status' in error))
|
|
41
|
+
return undefined;
|
|
42
|
+
return typeof error.status === 'number' ? error.status : undefined;
|
|
43
|
+
}
|
|
@@ -18,11 +18,14 @@ export class ProviderRegistry {
|
|
|
18
18
|
throw new Error(`Provider ${provider} is not registered`);
|
|
19
19
|
const adapter = adapterId(name);
|
|
20
20
|
try {
|
|
21
|
-
instances.push(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
instances.push({
|
|
22
|
+
...definition.create({
|
|
23
|
+
adapter,
|
|
24
|
+
config: definition.parseConfig(entry),
|
|
25
|
+
...(services === undefined ? {} : { services }),
|
|
26
|
+
}),
|
|
27
|
+
provider,
|
|
28
|
+
});
|
|
26
29
|
}
|
|
27
30
|
catch (error) {
|
|
28
31
|
failures.push({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitHubEventType } from '../contracts/events.js';
|
|
2
|
+
import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
|
|
2
3
|
import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
|
|
3
4
|
import { issueObservation } from './issue-source.js';
|
|
4
5
|
import { loadWatermark, overlapSince, reportPartialPollFailure, watermarkCheckpoint, } from './poll-watermark.js';
|
|
@@ -7,6 +8,7 @@ import { createGitHubRequestCoordinator, } from './request-coordinator.js';
|
|
|
7
8
|
export function createGitHubSource(config, client, adapter, requests = createGitHubRequestCoordinator({
|
|
8
9
|
maxConcurrent: config.polling.maxConcurrent,
|
|
9
10
|
}), state) {
|
|
11
|
+
const health = state?.health ?? createGitHubAdapterHealthRegistry(config.repositories);
|
|
10
12
|
// Draft eventIds are already content fingerprints (see issue-source.ts/pr-source.ts),
|
|
11
13
|
// so the journal itself is idempotent per item. This cache only avoids re-appending
|
|
12
14
|
// (and re-triggering downstream translation) an unchanged item on every poll within
|
|
@@ -25,6 +27,7 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
25
27
|
repo,
|
|
26
28
|
watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
|
|
27
29
|
now: state?.now ?? Date.now,
|
|
30
|
+
health,
|
|
28
31
|
})));
|
|
29
32
|
for (const result of perRepository)
|
|
30
33
|
if (result.succeeded)
|
|
@@ -40,10 +43,11 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
40
43
|
});
|
|
41
44
|
},
|
|
42
45
|
async markPollPersisted() {
|
|
43
|
-
if (state === undefined)
|
|
46
|
+
if (state?.checkpoints === undefined)
|
|
44
47
|
return;
|
|
48
|
+
const checkpoints = state.checkpoints;
|
|
45
49
|
for (const [repository, watermark] of pendingWatermarks) {
|
|
46
|
-
await
|
|
50
|
+
await checkpoints.save(watermarkCheckpoint(adapter, repository), watermark);
|
|
47
51
|
pendingWatermarks.delete(repository);
|
|
48
52
|
}
|
|
49
53
|
},
|
|
@@ -72,7 +76,7 @@ function limitGitHubSourceClient(client, requests) {
|
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
async function pollRepository(input) {
|
|
75
|
-
const { client, config, adapter, signal, owner, repo } = input;
|
|
79
|
+
const { client, config, adapter, signal, owner, repo, health } = input;
|
|
76
80
|
const queriedAt = input.now();
|
|
77
81
|
const context = {
|
|
78
82
|
client,
|
|
@@ -87,10 +91,18 @@ async function pollRepository(input) {
|
|
|
87
91
|
client.listIssues(owner, repo, config.polling.maxPerRepo, since),
|
|
88
92
|
client.listPullRequests(owner, repo, config.polling.maxPerRepo),
|
|
89
93
|
]);
|
|
90
|
-
if (
|
|
94
|
+
if (isFulfilled(issuesResult))
|
|
95
|
+
health.recordSuccess(context.repository, 'poll');
|
|
96
|
+
else {
|
|
91
97
|
reportPartialPollFailure(context.repository, 'issues');
|
|
92
|
-
|
|
98
|
+
health.recordFailure(context.repository, 'poll', issuesResult.reason);
|
|
99
|
+
}
|
|
100
|
+
if (isFulfilled(pullRequestsResult))
|
|
101
|
+
health.recordSuccess(context.repository, 'poll');
|
|
102
|
+
else {
|
|
93
103
|
reportPartialPollFailure(context.repository, 'pull requests');
|
|
104
|
+
health.recordFailure(context.repository, 'poll', pullRequestsResult.reason);
|
|
105
|
+
}
|
|
94
106
|
const issues = isFulfilled(issuesResult) ? issuesResult.value : [];
|
|
95
107
|
const pullRequestPayloads = isFulfilled(pullRequestsResult)
|
|
96
108
|
? pullRequestsResult.value.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since)
|
|
@@ -6,6 +6,7 @@ import { translateGitHubOutbound } from './application/outbound-translator.js';
|
|
|
6
6
|
import { createGitHubWakeLabelReconciler } from './application/wake-labels.js';
|
|
7
7
|
import { gitHubConfigSchema } from './contracts/config.js';
|
|
8
8
|
import { GitHubEventType } from './contracts/events.js';
|
|
9
|
+
import { createGitHubAdapterHealthRegistry } from './infrastructure/adapter-health-registry.js';
|
|
9
10
|
import { createGitHubClient } from './infrastructure/client.js';
|
|
10
11
|
import { createGitHubDelivery } from './infrastructure/delivery.js';
|
|
11
12
|
import { resolveGitHubCliToken } from './infrastructure/gh-auth.js';
|
|
@@ -24,12 +25,14 @@ export const gitHubProviderDefinition = {
|
|
|
24
25
|
const requests = createGitHubRequestCoordinator({
|
|
25
26
|
maxConcurrent: config.polling.maxConcurrent,
|
|
26
27
|
});
|
|
28
|
+
const health = createGitHubAdapterHealthRegistry(config.repositories);
|
|
27
29
|
return {
|
|
28
30
|
adapter,
|
|
29
31
|
eventTypes: Object.values(GitHubEventType),
|
|
30
32
|
source: createGitHubSource(config, client, adapter, requests, {
|
|
31
33
|
checkpoints: services.checkpoints,
|
|
32
34
|
now: () => services.clock.now().getTime(),
|
|
35
|
+
health,
|
|
33
36
|
}),
|
|
34
37
|
maintenance: createGitHubWakeLabelReconciler({
|
|
35
38
|
orchestration: services.orchestration,
|
|
@@ -39,16 +42,29 @@ export const gitHubProviderDefinition = {
|
|
|
39
42
|
setLabels: client.setIssueLabels,
|
|
40
43
|
requests,
|
|
41
44
|
}),
|
|
45
|
+
health: () => health.snapshotAll(),
|
|
42
46
|
delivery: createGitHubDelivery(async (intent, idempotencyKey) => {
|
|
43
47
|
const resource = await services.resources.get(resourceId(intent.resourceId));
|
|
44
48
|
if (resource === null)
|
|
45
49
|
throw new Error(`GitHub resource ${intent.resourceId} is unavailable`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const parsedKey = parsePullRequestKey(resource.externalKey.key);
|
|
51
|
+
const repository = parsedKey === null ? null : `${parsedKey.owner}/${parsedKey.repo}`;
|
|
52
|
+
try {
|
|
53
|
+
const externalId = await client.deliver({
|
|
54
|
+
...translateGitHubOutbound(resource, intent, {
|
|
55
|
+
publicUiUrl: services.publicUiUrl,
|
|
56
|
+
}),
|
|
57
|
+
idempotencyKey,
|
|
58
|
+
});
|
|
59
|
+
if (repository !== null)
|
|
60
|
+
health.recordSuccess(repository, 'deliver');
|
|
61
|
+
return externalId;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (repository !== null)
|
|
65
|
+
health.recordFailure(repository, 'deliver', error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
52
68
|
}, async (intent) => {
|
|
53
69
|
if (intent.kind !== BuiltInActivityName.IssueComplete)
|
|
54
70
|
return null;
|