@usine/orchestrator 0.1.0 → 0.2.0
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/README.md +44 -21
- package/dist/board/items.d.ts +18 -0
- package/dist/board/items.d.ts.map +1 -0
- package/dist/board/items.js +46 -0
- package/dist/board/items.js.map +1 -0
- package/dist/board/projection.d.ts +6 -5
- package/dist/board/projection.d.ts.map +1 -1
- package/dist/board/projection.js +25 -65
- package/dist/board/projection.js.map +1 -1
- package/dist/board/tasks.d.ts +8 -0
- package/dist/board/tasks.d.ts.map +1 -0
- package/dist/board/tasks.js +86 -0
- package/dist/board/tasks.js.map +1 -0
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +6 -6
- package/dist/credentials.js.map +1 -1
- package/dist/exe-dev.d.ts +5 -4
- package/dist/exe-dev.d.ts.map +1 -1
- package/dist/exe-dev.js +5 -4
- package/dist/exe-dev.js.map +1 -1
- package/dist/idle-triggers.d.ts +1 -1
- package/dist/idle-triggers.d.ts.map +1 -1
- package/dist/idle-triggers.js +3 -1
- package/dist/idle-triggers.js.map +1 -1
- package/dist/label-reconciler.d.ts +30 -0
- package/dist/label-reconciler.d.ts.map +1 -0
- package/dist/label-reconciler.js +119 -0
- package/dist/label-reconciler.js.map +1 -0
- package/dist/local.d.ts +17 -12
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +20 -13
- package/dist/local.js.map +1 -1
- package/dist/operator/attempt.d.ts +66 -0
- package/dist/operator/attempt.d.ts.map +1 -0
- package/dist/operator/attempt.js +58 -0
- package/dist/operator/attempt.js.map +1 -0
- package/dist/operator/board.d.ts +119 -0
- package/dist/operator/board.d.ts.map +1 -0
- package/dist/operator/board.js +101 -0
- package/dist/operator/board.js.map +1 -0
- package/dist/operator/http.d.ts +114 -0
- package/dist/operator/http.d.ts.map +1 -0
- package/dist/operator/http.js +139 -0
- package/dist/operator/http.js.map +1 -0
- package/dist/operator/need.d.ts +66 -0
- package/dist/operator/need.d.ts.map +1 -0
- package/dist/operator/need.js +70 -0
- package/dist/operator/need.js.map +1 -0
- package/dist/operator/operator.d.ts +64 -0
- package/dist/operator/operator.d.ts.map +1 -0
- package/dist/operator/{service.js → operator.js} +147 -32
- package/dist/operator/operator.js.map +1 -0
- package/dist/operator/overview.d.ts +51 -0
- package/dist/operator/overview.d.ts.map +1 -0
- package/dist/operator/overview.js +48 -0
- package/dist/operator/overview.js.map +1 -0
- package/dist/operator/projection.d.ts +9 -9
- package/dist/operator/projection.d.ts.map +1 -1
- package/dist/operator/projection.js +18 -10
- package/dist/operator/projection.js.map +1 -1
- package/dist/operator/run.d.ts +38 -0
- package/dist/operator/run.d.ts.map +1 -0
- package/dist/operator/run.js +41 -0
- package/dist/operator/run.js.map +1 -0
- package/dist/operator/transcript.d.ts +57 -0
- package/dist/operator/transcript.d.ts.map +1 -0
- package/dist/operator/transcript.js +59 -0
- package/dist/operator/transcript.js.map +1 -0
- package/dist/orchestrator.d.ts +3 -2
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1 -1
- package/dist/orchestrator.js.map +1 -1
- package/dist/polling/cycle.d.ts +51 -0
- package/dist/polling/cycle.d.ts.map +1 -0
- package/dist/polling/cycle.js +44 -0
- package/dist/polling/cycle.js.map +1 -0
- package/dist/polling/hints.d.ts +20 -0
- package/dist/polling/hints.d.ts.map +1 -0
- package/dist/polling/hints.js +54 -0
- package/dist/polling/hints.js.map +1 -0
- package/dist/polling/loop-evaluation.d.ts +21 -0
- package/dist/polling/loop-evaluation.d.ts.map +1 -0
- package/dist/polling/loop-evaluation.js +131 -0
- package/dist/polling/loop-evaluation.js.map +1 -0
- package/dist/polling/polling.d.ts +36 -0
- package/dist/polling/polling.d.ts.map +1 -0
- package/dist/polling/polling.js +296 -0
- package/dist/polling/polling.js.map +1 -0
- package/dist/polling/repository-sync.d.ts +45 -0
- package/dist/polling/repository-sync.d.ts.map +1 -0
- package/dist/polling/repository-sync.js +169 -0
- package/dist/polling/repository-sync.js.map +1 -0
- package/dist/polling-triggers.d.ts +3 -39
- package/dist/polling-triggers.d.ts.map +1 -1
- package/dist/polling-triggers.js +3 -393
- package/dist/polling-triggers.js.map +1 -1
- package/dist/schedule-triggers.d.ts +17 -15
- package/dist/schedule-triggers.d.ts.map +1 -1
- package/dist/schedule-triggers.js +53 -43
- package/dist/schedule-triggers.js.map +1 -1
- package/dist/scheduler.d.ts +5 -5
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +28 -43
- package/dist/scheduler.js.map +1 -1
- package/dist/trigger-submission.d.ts +33 -24
- package/dist/trigger-submission.d.ts.map +1 -1
- package/dist/trigger-submission.js +32 -29
- package/dist/trigger-submission.js.map +1 -1
- package/package.json +50 -14
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -11
- package/dist/index.js.map +0 -1
- package/dist/operator/model.d.ts +0 -280
- package/dist/operator/model.d.ts.map +0 -1
- package/dist/operator/model.js +0 -242
- package/dist/operator/model.js.map +0 -1
- package/dist/operator/service.d.ts +0 -36
- package/dist/operator/service.d.ts.map +0 -1
- package/dist/operator/service.js.map +0 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { CycleGeneration } from "@usine/core/cycle-budget";
|
|
2
|
+
import { CycleBudgets } from "@usine/engine/cycle-budget";
|
|
3
|
+
import { ItemKey, ItemNumber } from "@usine/core/item";
|
|
4
|
+
import { RepositoryName, canonicalRepositoryName } from "@usine/core/repository";
|
|
5
|
+
import { ItemContentHash, ItemProjection, ItemProjectionSource, ItemProjectionWriter, } from "@usine/engine/item-projection";
|
|
6
|
+
import { RepositoryLabel, RepositoryLabelProjection, RepositoryLabelProjectionReader, RepositoryLabelProjectionWriter, } from "@usine/engine/repository-label-projection";
|
|
7
|
+
import { EntityTag, GitHubPolling, RepositoryLabelCatalog, } from "@usine/github/polling";
|
|
8
|
+
import { DateTime, Effect, Option, Result, Schema } from "effect";
|
|
9
|
+
import { LabelReconciler } from "../label-reconciler.js";
|
|
10
|
+
import { latestPollAt, RepositoryPollFailure, repositoryFailureLogFields, } from "./cycle.js";
|
|
11
|
+
/**
|
|
12
|
+
* Identifies one GitHub issue or pull request across its changing snapshots.
|
|
13
|
+
* Encoding uses the canonical repository name and Item number; decoding lets
|
|
14
|
+
* Board consumers locate the Item associated with a Run. Schedule Runs identify
|
|
15
|
+
* the repository alone and do not decode as an Item.
|
|
16
|
+
*/
|
|
17
|
+
export const GitHubItemKey = Schema.toType(ItemKey).pipe(Schema.decodeTo(Schema.fromJsonString(Schema.Tuple([Schema.Literal("github"), RepositoryName, ItemNumber]))));
|
|
18
|
+
// `toType` preserves the ItemKey brand on the encoded side, so the encoder
|
|
19
|
+
// produces the identity accepted by both the Board projection and Run engine.
|
|
20
|
+
const encodeGitHubItemKey = Schema.encodeSync(GitHubItemKey);
|
|
21
|
+
/** The reconciliation result when the cycle had no confirmed catalog to compare against. */
|
|
22
|
+
const DEFERRED_RECONCILIATION = {
|
|
23
|
+
labels: [],
|
|
24
|
+
findings: [],
|
|
25
|
+
catalogChanged: false,
|
|
26
|
+
nextPollAt: Option.none(),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Refreshes the stored label catalog that the Board and the label
|
|
30
|
+
* preflight read. A failed request keeps the stored catalog and ETag and
|
|
31
|
+
* defers provisioning; trigger evaluation runs from the change poll alone.
|
|
32
|
+
*/
|
|
33
|
+
const applyLabelCatalog = Effect.fn("PollingTriggers.applyLabelCatalog")(function* (repository, labelEntityTag, result) {
|
|
34
|
+
yield* Effect.annotateCurrentSpan({ repository });
|
|
35
|
+
const repositoryLabelReader = yield* RepositoryLabelProjectionReader;
|
|
36
|
+
const repositoryLabelWriter = yield* RepositoryLabelProjectionWriter;
|
|
37
|
+
const unchanged = {
|
|
38
|
+
labelEntityTag,
|
|
39
|
+
entityTagChanged: false,
|
|
40
|
+
confirmedLabels: Option.none(),
|
|
41
|
+
failures: [],
|
|
42
|
+
nextPollAt: Option.none(),
|
|
43
|
+
};
|
|
44
|
+
if (Result.isFailure(result)) {
|
|
45
|
+
const error = result.failure;
|
|
46
|
+
if (error._tag === "GitHubRateLimitError") {
|
|
47
|
+
yield* Effect.logWarning("Label catalog rate-limited; kept the stored catalog and deferred label provisioning", {
|
|
48
|
+
repository,
|
|
49
|
+
operation: error.operation,
|
|
50
|
+
retryAt: DateTime.formatIso(error.retryAt),
|
|
51
|
+
});
|
|
52
|
+
return { ...unchanged, nextPollAt: Option.some(error.retryAt) };
|
|
53
|
+
}
|
|
54
|
+
yield* Effect.logError("Label catalog request failed; kept the stored catalog, deferred label provisioning, and evaluated items anyway", {
|
|
55
|
+
repository,
|
|
56
|
+
...repositoryFailureLogFields(error),
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
...unchanged,
|
|
60
|
+
failures: [
|
|
61
|
+
new RepositoryPollFailure({
|
|
62
|
+
repository,
|
|
63
|
+
errorTag: error._tag,
|
|
64
|
+
message: error.message,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const detection = result.success;
|
|
70
|
+
if (!(detection instanceof RepositoryLabelCatalog)) {
|
|
71
|
+
const projection = yield* repositoryLabelReader.get(repository);
|
|
72
|
+
if (Option.isNone(projection)) {
|
|
73
|
+
// The ETag vouches for a projection that is no longer stored, so drop
|
|
74
|
+
// it and fetch the catalog in full next cycle.
|
|
75
|
+
yield* Effect.logWarning("Repository label catalog unchanged (304) but its projection is missing; refetching next cycle and deferring label provisioning", { repository });
|
|
76
|
+
return { ...unchanged, labelEntityTag: Option.none(), entityTagChanged: true };
|
|
77
|
+
}
|
|
78
|
+
yield* Effect.logDebug("Repository label catalog unchanged (304)", {
|
|
79
|
+
repository,
|
|
80
|
+
});
|
|
81
|
+
return { ...unchanged, confirmedLabels: Option.some(projection.value.labels) };
|
|
82
|
+
}
|
|
83
|
+
const labels = detection.labels.map(({ name, color, description }) => new RepositoryLabel({ name, color: Option.some(color), description }));
|
|
84
|
+
yield* repositoryLabelWriter.replace(new RepositoryLabelProjection({ repository, labels }));
|
|
85
|
+
return {
|
|
86
|
+
...unchanged,
|
|
87
|
+
labelEntityTag: detection.entityTag,
|
|
88
|
+
entityTagChanged: true,
|
|
89
|
+
confirmedLabels: Option.some(labels),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Refreshes and reconciles one repository's Board label catalog before change
|
|
94
|
+
* detection. Pass the catalog ETag from its stored poll cursor; the result gives
|
|
95
|
+
* the ETag to persist after Item processing, plus preflight findings and backoff.
|
|
96
|
+
* Failed catalog requests retain the stored projection and defer provisioning;
|
|
97
|
+
* persistence failures stop the repository for retry.
|
|
98
|
+
*/
|
|
99
|
+
export const synchronizeLabels = Effect.fn("PollingTriggers.synchronizeLabels")(function* (repository, declared, entityTag) {
|
|
100
|
+
yield* Effect.annotateCurrentSpan({ repository });
|
|
101
|
+
const github = yield* GitHubPolling;
|
|
102
|
+
const labelReconciler = yield* LabelReconciler;
|
|
103
|
+
const repositoryLabelWriter = yield* RepositoryLabelProjectionWriter;
|
|
104
|
+
const result = yield* Effect.result(github.labels(repository, entityTag));
|
|
105
|
+
const catalog = yield* applyLabelCatalog(repository, entityTag, result);
|
|
106
|
+
const reconciliation = yield* Option.match(catalog.confirmedLabels, {
|
|
107
|
+
onNone: () => Effect.succeed(DEFERRED_RECONCILIATION),
|
|
108
|
+
onSome: (confirmed) => labelReconciler.reconcile(repository, declared, confirmed),
|
|
109
|
+
});
|
|
110
|
+
const preflight = {
|
|
111
|
+
labelEntityTag: reconciliation.catalogChanged
|
|
112
|
+
? Option.none()
|
|
113
|
+
: catalog.labelEntityTag,
|
|
114
|
+
entityTagChanged: catalog.entityTagChanged || reconciliation.catalogChanged,
|
|
115
|
+
failures: catalog.failures,
|
|
116
|
+
preflightFindings: reconciliation.findings,
|
|
117
|
+
nextPollAt: latestPollAt([catalog.nextPollAt, reconciliation.nextPollAt]),
|
|
118
|
+
};
|
|
119
|
+
if (reconciliation.catalogChanged) {
|
|
120
|
+
yield* repositoryLabelWriter
|
|
121
|
+
.replace(new RepositoryLabelProjection({ repository, labels: reconciliation.labels }))
|
|
122
|
+
.pipe(
|
|
123
|
+
// Provisioning may have reported failures before projection storage
|
|
124
|
+
// fails. Preserve those findings just as for a later Item failure.
|
|
125
|
+
Effect.mapError((error) => ({
|
|
126
|
+
_tag: "InterruptedRepositoryCycle",
|
|
127
|
+
error,
|
|
128
|
+
failures: preflight.failures,
|
|
129
|
+
preflightFindings: preflight.preflightFindings,
|
|
130
|
+
nextPollAt: preflight.nextPollAt,
|
|
131
|
+
})));
|
|
132
|
+
}
|
|
133
|
+
return preflight;
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* Fetches a detected Item and durably updates its Board projection regardless of
|
|
137
|
+
* whether any Loop applies. Evaluation uses the returned projection's snapshot,
|
|
138
|
+
* identity and content hash, so Board observation and Run decisions agree.
|
|
139
|
+
*/
|
|
140
|
+
export const observeItem = Effect.fn("PollingTriggers.observeItem")(function* (repository, changed) {
|
|
141
|
+
yield* Effect.annotateCurrentSpan({ repository, item: changed.number });
|
|
142
|
+
const github = yield* GitHubPolling;
|
|
143
|
+
const items = yield* ItemProjectionWriter;
|
|
144
|
+
const budgets = yield* CycleBudgets;
|
|
145
|
+
const itemKey = encodeGitHubItemKey([
|
|
146
|
+
"github",
|
|
147
|
+
RepositoryName.make(canonicalRepositoryName(repository)),
|
|
148
|
+
ItemNumber.make(changed.number),
|
|
149
|
+
]);
|
|
150
|
+
const cycleGeneration = yield* budgets.generation(itemKey);
|
|
151
|
+
const snapshot = yield* github.snapshot(repository, changed);
|
|
152
|
+
const projection = new ItemProjection({
|
|
153
|
+
itemKey,
|
|
154
|
+
repository,
|
|
155
|
+
source: ItemProjectionSource.make(changed._tag === "ChangedIssue" ? "issue" : "pr"),
|
|
156
|
+
number: ItemNumber.make(changed.number),
|
|
157
|
+
contentHash: ItemContentHash.make(snapshot.contentHash),
|
|
158
|
+
payload: snapshot.payload,
|
|
159
|
+
});
|
|
160
|
+
yield* items.upsert(projection);
|
|
161
|
+
yield* Effect.logDebug("Fetched changed-item snapshot", {
|
|
162
|
+
repository,
|
|
163
|
+
item: projection.number,
|
|
164
|
+
stream: projection.source,
|
|
165
|
+
contentHash: projection.contentHash,
|
|
166
|
+
});
|
|
167
|
+
return { projection, cycleGeneration };
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=repository-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository-sync.js","sourceRoot":"","sources":["../../src/polling/repository-sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,40 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { GitHubPolling } from "@usine/github";
|
|
5
|
-
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
6
|
-
declare const RepositoryPollFailure_base: Schema.Class<RepositoryPollFailure, Schema.Struct<{
|
|
7
|
-
readonly repository: Schema.brand<Schema.String, "RepoName">;
|
|
8
|
-
readonly errorTag: Schema.NonEmptyString;
|
|
9
|
-
readonly message: Schema.String;
|
|
10
|
-
}>, {}>;
|
|
11
|
-
/** One repository whose cycle ended in a typed error; siblings were unaffected. */
|
|
12
|
-
export declare class RepositoryPollFailure extends RepositoryPollFailure_base {
|
|
13
|
-
}
|
|
14
|
-
declare const PollCycle_base: Schema.Class<PollCycle, Schema.Struct<{
|
|
15
|
-
readonly repositories: Schema.Int;
|
|
16
|
-
readonly changedItems: Schema.Int;
|
|
17
|
-
readonly submissions: Schema.$Array<Schema.Union<readonly [typeof import("@usine/engine").Claimed, typeof import("@usine/engine").Queued, typeof import("@usine/engine").Ignored]>>;
|
|
18
|
-
/** Run ids actually revoked because their item stopped matching. */
|
|
19
|
-
readonly revokedRuns: Schema.Int;
|
|
20
|
-
readonly failures: Schema.$Array<typeof RepositoryPollFailure>;
|
|
21
|
-
readonly nextPollAt: Schema.Option<Schema.DateTimeUtc>;
|
|
22
|
-
}>, {}>;
|
|
23
|
-
export declare class PollCycle extends PollCycle_base {
|
|
24
|
-
}
|
|
25
|
-
declare const PollingTriggers_base: Context.ServiceClass<PollingTriggers, "@usine/orchestrator/PollingTriggers", {
|
|
26
|
-
/**
|
|
27
|
-
* One cycle over every watched repository. Per-repository failures are
|
|
28
|
-
* isolated, logged, and reported on the cycle — one bad repository never
|
|
29
|
-
* stops its siblings, and only a defect stops the loop.
|
|
30
|
-
*/
|
|
31
|
-
readonly poll: (instance: LoadedInstance) => Effect.Effect<PollCycle, EnginePersistenceError>;
|
|
32
|
-
readonly run: (instance: LoadedInstance) => Effect.Effect<never>;
|
|
33
|
-
readonly latest: Effect.Effect<Option.Option<PollCycle>>;
|
|
34
|
-
}>;
|
|
35
|
-
/** Full selector-pruned GitHub change → snapshot → CEL → Event composition. */
|
|
36
|
-
export declare class PollingTriggers extends PollingTriggers_base {
|
|
37
|
-
static readonly layer: Layer.Layer<PollingTriggers, never, ConfigParser | CursorStore | ExpressionEngine | GitHubPolling | ItemProjectionWriter | LoopControl | RepositoryLabelProjectionWriter | RunEngine>;
|
|
38
|
-
}
|
|
39
|
-
export {};
|
|
1
|
+
export { PollingTriggers } from "./polling/polling.ts";
|
|
2
|
+
export { PollCycle, RepositoryPollFailure } from "./polling/cycle.ts";
|
|
3
|
+
export { GitHubItemKey } from "./polling/repository-sync.ts";
|
|
40
4
|
//# sourceMappingURL=polling-triggers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"polling-triggers.d.ts","sourceRoot":"","sources":["../src/polling-triggers.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"polling-triggers.d.ts","sourceRoot":"","sources":["../src/polling-triggers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/polling-triggers.js
CHANGED
|
@@ -1,394 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { ExpressionEngine } from "@usine/expression";
|
|
5
|
-
import { GitHubPolling, EntityTag, PollCursor, RepositoryLabelCatalog, } from "@usine/github";
|
|
6
|
-
import { Clock, Context, DateTime, Duration, Effect, Layer, Option, Ref, Result, Schema, } from "effect";
|
|
7
|
-
import { configFileErrorLogFields, resolveTriggerLoops, submissionLogFields, triggerSubmission, } from "./trigger-submission.js";
|
|
8
|
-
/** GitHub REST + GraphQL tolerate a handful of parallel calls per token. */
|
|
9
|
-
const REPOSITORY_CONCURRENCY = 4;
|
|
10
|
-
const ITEM_CONCURRENCY = 4;
|
|
11
|
-
const WatchKey = Schema.NonEmptyString.pipe(Schema.brand("GitHubPollWatchKey"));
|
|
12
|
-
class StoredPollCursor extends Schema.Class("@usine/orchestrator/StoredPollCursor")({
|
|
13
|
-
boardVersion: Schema.Literal(2),
|
|
14
|
-
cursor: PollCursor,
|
|
15
|
-
labelEntityTag: Schema.OptionFromNullOr(EntityTag),
|
|
16
|
-
watches: Schema.Array(WatchKey).check(Schema.isUnique()),
|
|
17
|
-
}) {
|
|
18
|
-
}
|
|
19
|
-
/** One repository whose cycle ended in a typed error; siblings were unaffected. */
|
|
20
|
-
export class RepositoryPollFailure extends Schema.Class("@usine/orchestrator/RepositoryPollFailure")({
|
|
21
|
-
repository: Repository.Name,
|
|
22
|
-
errorTag: Schema.NonEmptyString,
|
|
23
|
-
message: Schema.String,
|
|
24
|
-
}) {
|
|
25
|
-
}
|
|
26
|
-
export class PollCycle extends Schema.Class("@usine/orchestrator/PollCycle")({
|
|
27
|
-
repositories: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
28
|
-
changedItems: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
29
|
-
submissions: Schema.Array(SubmissionResult),
|
|
30
|
-
/** Run ids actually revoked because their item stopped matching. */
|
|
31
|
-
revokedRuns: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
32
|
-
failures: Schema.Array(RepositoryPollFailure),
|
|
33
|
-
nextPollAt: Schema.Option(Schema.DateTimeUtc),
|
|
34
|
-
}) {
|
|
35
|
-
}
|
|
36
|
-
const repositoryFailureLogFields = (error) => {
|
|
37
|
-
const common = { errorTag: error._tag, message: error.message };
|
|
38
|
-
if (error._tag === "ConfigFileError") {
|
|
39
|
-
return { ...common, ...configFileErrorLogFields(error) };
|
|
40
|
-
}
|
|
41
|
-
if (error._tag === "ExpressionCheckError" || error._tag === "ExpressionEvaluationError") {
|
|
42
|
-
return { ...common, expression: error.source };
|
|
43
|
-
}
|
|
44
|
-
return common;
|
|
45
|
-
};
|
|
46
|
-
const cursorKey = (repository) => CursorKey.make(`github-poll:${Repository.canonical(repository.name)}`);
|
|
47
|
-
const watchKey = ({ loop }) => WatchKey.make(JSON.stringify([loop.name, loop.trigger.on, loop.trigger.if]));
|
|
48
|
-
const watchedRepositories = (instance, resolved) => instance.config.repos.map((repository) => {
|
|
49
|
-
const loops = resolved.filter(({ loop }) => loop.trigger.on !== "schedule" && matchesSelector(loop.selector, repository));
|
|
50
|
-
return { repository, loops, watches: loops.map(watchKey).sort() };
|
|
51
|
-
});
|
|
52
|
-
const sameWatches = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
53
|
-
const itemStream = (item) => item._tag === "ChangedIssue" ? "issue" : "pr";
|
|
54
|
-
const itemKey = (repository, item) => Item.ItemKey.make(JSON.stringify(["github", Repository.canonical(repository.name), item.number]));
|
|
55
|
-
const eventKey = (loop, repository, item, snapshot) => Event.DedupKey.make(JSON.stringify([
|
|
56
|
-
loop.name,
|
|
57
|
-
Repository.canonical(repository.name),
|
|
58
|
-
item.number,
|
|
59
|
-
snapshot.contentHash,
|
|
60
|
-
]));
|
|
61
|
-
const latestPollAt = (values) => {
|
|
62
|
-
const epochs = values.flatMap((value) => Option.match(value, {
|
|
63
|
-
onNone: () => [],
|
|
64
|
-
onSome: (dateTime) => [DateTime.toEpochMillis(dateTime)],
|
|
65
|
-
}));
|
|
66
|
-
return epochs.length === 0 ? Option.none() : DateTime.make(Math.max(...epochs));
|
|
67
|
-
};
|
|
68
|
-
/** Full selector-pruned GitHub change → snapshot → CEL → Event composition. */
|
|
69
|
-
export class PollingTriggers extends Context.Service()("@usine/orchestrator/PollingTriggers") {
|
|
70
|
-
static layer = Layer.effect(PollingTriggers, Effect.gen(function* () {
|
|
71
|
-
const cursors = yield* CursorStore;
|
|
72
|
-
const config = yield* ConfigParser;
|
|
73
|
-
const expressions = yield* ExpressionEngine;
|
|
74
|
-
const github = yield* GitHubPolling;
|
|
75
|
-
const items = yield* ItemProjectionWriter;
|
|
76
|
-
const repositoryLabels = yield* RepositoryLabelProjectionWriter;
|
|
77
|
-
const runs = yield* RunEngine;
|
|
78
|
-
const loops = yield* LoopControl;
|
|
79
|
-
const latest = yield* Ref.make(Option.none());
|
|
80
|
-
const emptyCursor = (watches) => new StoredPollCursor({
|
|
81
|
-
boardVersion: 2,
|
|
82
|
-
cursor: PollCursor.empty,
|
|
83
|
-
labelEntityTag: Option.none(),
|
|
84
|
-
watches,
|
|
85
|
-
});
|
|
86
|
-
const readCursor = Effect.fn("PollingTriggers.readCursor")(function* (watched) {
|
|
87
|
-
const stored = yield* cursors.read(cursorKey(watched.repository), StoredPollCursor);
|
|
88
|
-
if (Option.isNone(stored))
|
|
89
|
-
return emptyCursor(watched.watches);
|
|
90
|
-
if (!sameWatches(stored.value.watches, watched.watches)) {
|
|
91
|
-
yield* Effect.logInfo("Watched loops changed; re-polling the repository from scratch", {
|
|
92
|
-
repository: watched.repository.name,
|
|
93
|
-
previousWatches: stored.value.watches.length,
|
|
94
|
-
currentWatches: watched.watches.length,
|
|
95
|
-
});
|
|
96
|
-
return emptyCursor(watched.watches);
|
|
97
|
-
}
|
|
98
|
-
return stored.value;
|
|
99
|
-
});
|
|
100
|
-
const writeCursor = Effect.fn("PollingTriggers.writeCursor")((watched, cursor) => cursors.write(cursorKey(watched.repository), StoredPollCursor, cursor));
|
|
101
|
-
const evaluateItem = Effect.fn("PollingTriggers.evaluateItem")(function* (watched, item) {
|
|
102
|
-
yield* Effect.annotateCurrentSpan({
|
|
103
|
-
repository: watched.repository.name,
|
|
104
|
-
item: item.number,
|
|
105
|
-
});
|
|
106
|
-
const loops = watched.loops.filter(({ loop }) => loop.trigger.on === itemStream(item));
|
|
107
|
-
const snapshot = yield* github.snapshot(watched.repository.name, item);
|
|
108
|
-
// Both snapshot variants carry their payload; the union needs no recheck.
|
|
109
|
-
const payload = snapshot.payload;
|
|
110
|
-
const context = { repo: repositoryContext(watched.repository), item: payload };
|
|
111
|
-
const stableItemKey = itemKey(watched.repository, item);
|
|
112
|
-
yield* items.upsert(new ItemProjection({
|
|
113
|
-
itemKey: stableItemKey,
|
|
114
|
-
repository: watched.repository.name,
|
|
115
|
-
source: ItemProjectionSource.make(itemStream(item)),
|
|
116
|
-
number: ItemNumber.make(item.number),
|
|
117
|
-
contentHash: ItemContentHash.make(snapshot.contentHash),
|
|
118
|
-
payload,
|
|
119
|
-
}));
|
|
120
|
-
yield* Effect.logDebug("Fetched changed-item snapshot", {
|
|
121
|
-
repository: watched.repository.name,
|
|
122
|
-
item: item.number,
|
|
123
|
-
stream: itemStream(item),
|
|
124
|
-
contentHash: snapshot.contentHash,
|
|
125
|
-
});
|
|
126
|
-
if (loops.length === 0) {
|
|
127
|
-
yield* Effect.logDebug("Item projected without any matching repository Loop", {
|
|
128
|
-
repository: watched.repository.name,
|
|
129
|
-
item: item.number,
|
|
130
|
-
stream: itemStream(item),
|
|
131
|
-
});
|
|
132
|
-
return { submissions: [], revokedRuns: 0, failures: [] };
|
|
133
|
-
}
|
|
134
|
-
const submissions = [];
|
|
135
|
-
const failures = [];
|
|
136
|
-
let revokedRuns = 0;
|
|
137
|
-
for (const { agent, loop, parentModel } of loops) {
|
|
138
|
-
yield* Effect.annotateCurrentSpan({ loop: loop.name, agent: agent.name });
|
|
139
|
-
const matches = yield* expressions.evaluateBoolean(loop.trigger.if, environmentFor(loop.trigger.on), context);
|
|
140
|
-
if (!matches) {
|
|
141
|
-
yield* Effect.logDebug("Item did not match its loop expression", {
|
|
142
|
-
repository: watched.repository.name,
|
|
143
|
-
loop: loop.name,
|
|
144
|
-
agent: agent.name,
|
|
145
|
-
item: item.number,
|
|
146
|
-
});
|
|
147
|
-
const revoked = yield* runs.revoke(loop.name, stableItemKey);
|
|
148
|
-
revokedRuns += revoked.length;
|
|
149
|
-
if (revoked.length > 0) {
|
|
150
|
-
yield* Effect.logInfo("Item stopped matching; revoked its unstarted runs", {
|
|
151
|
-
repository: watched.repository.name,
|
|
152
|
-
loop: loop.name,
|
|
153
|
-
agent: agent.name,
|
|
154
|
-
item: item.number,
|
|
155
|
-
runIds: revoked.join(", "),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const dedupKey = eventKey(loop, watched.repository, item, snapshot);
|
|
161
|
-
const receivedAt = yield* DateTime.now;
|
|
162
|
-
// A render failure is deterministic loop authoring, not transport:
|
|
163
|
-
// re-polling the same snapshot re-fails identically. Isolate it to
|
|
164
|
-
// this loop so sibling loops and the cursor still advance.
|
|
165
|
-
const rendered = yield* Effect.result(config.renderPrompt(loop, context));
|
|
166
|
-
if (Result.isFailure(rendered)) {
|
|
167
|
-
yield* Effect.logError("Loop prompt rendering failed; sibling loops were unaffected", {
|
|
168
|
-
loop: loop.name,
|
|
169
|
-
repository: watched.repository.name,
|
|
170
|
-
agent: agent.name,
|
|
171
|
-
item: item.number,
|
|
172
|
-
errorTag: rendered.failure._tag,
|
|
173
|
-
message: rendered.failure.message,
|
|
174
|
-
...configFileErrorLogFields(rendered.failure),
|
|
175
|
-
});
|
|
176
|
-
failures.push(new RepositoryPollFailure({
|
|
177
|
-
repository: watched.repository.name,
|
|
178
|
-
errorTag: rendered.failure._tag,
|
|
179
|
-
message: rendered.failure.message,
|
|
180
|
-
}));
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const submitted = yield* runs.submit(triggerSubmission({
|
|
184
|
-
dedupKey,
|
|
185
|
-
itemKey: stableItemKey,
|
|
186
|
-
loop,
|
|
187
|
-
payload,
|
|
188
|
-
receivedAt,
|
|
189
|
-
repository: watched.repository,
|
|
190
|
-
task: AttemptChannel.Task.make(rendered.success),
|
|
191
|
-
agent,
|
|
192
|
-
parentModel,
|
|
193
|
-
}));
|
|
194
|
-
yield* Effect.logInfo("Submitted matching item state", {
|
|
195
|
-
repository: watched.repository.name,
|
|
196
|
-
loop: loop.name,
|
|
197
|
-
agent: agent.name,
|
|
198
|
-
item: item.number,
|
|
199
|
-
contentHash: snapshot.contentHash,
|
|
200
|
-
result: submitted._tag,
|
|
201
|
-
...submissionLogFields(submitted),
|
|
202
|
-
});
|
|
203
|
-
submissions.push(submitted);
|
|
204
|
-
}
|
|
205
|
-
return { submissions, revokedRuns, failures };
|
|
206
|
-
});
|
|
207
|
-
/**
|
|
208
|
-
* The label catalog feeds the Board projection only. A failed catalog
|
|
209
|
-
* request keeps the stored catalog and ETag and never blocks trigger
|
|
210
|
-
* evaluation: label-triggered Loops fire from the change poll alone.
|
|
211
|
-
*/
|
|
212
|
-
const applyLabelCatalog = Effect.fn("PollingTriggers.applyLabelCatalog")(function* (watched, stored, result) {
|
|
213
|
-
const unchanged = {
|
|
214
|
-
labelEntityTag: stored.labelEntityTag,
|
|
215
|
-
stored: false,
|
|
216
|
-
failures: [],
|
|
217
|
-
nextPollAt: Option.none(),
|
|
218
|
-
};
|
|
219
|
-
if (Result.isFailure(result)) {
|
|
220
|
-
const error = result.failure;
|
|
221
|
-
if (error._tag === "GitHubRateLimitError") {
|
|
222
|
-
yield* Effect.logWarning("Label catalog rate-limited; keeping the stored catalog", {
|
|
223
|
-
repository: watched.repository.name,
|
|
224
|
-
operation: error.operation,
|
|
225
|
-
retryAt: DateTime.formatIso(error.retryAt),
|
|
226
|
-
});
|
|
227
|
-
return { ...unchanged, nextPollAt: Option.some(error.retryAt) };
|
|
228
|
-
}
|
|
229
|
-
yield* Effect.logError("Label catalog request failed; kept the stored catalog and evaluated items anyway", {
|
|
230
|
-
repository: watched.repository.name,
|
|
231
|
-
...repositoryFailureLogFields(error),
|
|
232
|
-
});
|
|
233
|
-
return {
|
|
234
|
-
...unchanged,
|
|
235
|
-
failures: [
|
|
236
|
-
new RepositoryPollFailure({
|
|
237
|
-
repository: watched.repository.name,
|
|
238
|
-
errorTag: error._tag,
|
|
239
|
-
message: error.message,
|
|
240
|
-
}),
|
|
241
|
-
],
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
const detection = result.success;
|
|
245
|
-
if (!(detection instanceof RepositoryLabelCatalog)) {
|
|
246
|
-
yield* Effect.logDebug("Repository label catalog unchanged (304)", {
|
|
247
|
-
repository: watched.repository.name,
|
|
248
|
-
});
|
|
249
|
-
return unchanged;
|
|
250
|
-
}
|
|
251
|
-
yield* repositoryLabels.replace(new RepositoryLabelProjection({
|
|
252
|
-
repository: watched.repository.name,
|
|
253
|
-
labels: detection.labels.map(String),
|
|
254
|
-
}));
|
|
255
|
-
return { ...unchanged, labelEntityTag: detection.entityTag, stored: true };
|
|
256
|
-
});
|
|
257
|
-
const pollRepository = Effect.fn("PollingTriggers.pollRepository")(function* (watched) {
|
|
258
|
-
yield* Effect.annotateCurrentSpan({ repository: watched.repository.name });
|
|
259
|
-
const stored = yield* readCursor(watched);
|
|
260
|
-
const [detected, labelResult] = yield* Effect.all([
|
|
261
|
-
github.changes(watched.repository.name, stored.cursor),
|
|
262
|
-
Effect.result(github.labels(watched.repository.name, stored.labelEntityTag)),
|
|
263
|
-
], { concurrency: "unbounded" });
|
|
264
|
-
const catalog = yield* applyLabelCatalog(watched, stored, labelResult);
|
|
265
|
-
const nextCursor = new StoredPollCursor({
|
|
266
|
-
boardVersion: 2,
|
|
267
|
-
cursor: detected.cursor,
|
|
268
|
-
labelEntityTag: catalog.labelEntityTag,
|
|
269
|
-
watches: watched.watches,
|
|
270
|
-
});
|
|
271
|
-
const nextPollAt = latestPollAt([detected.nextPollAt, catalog.nextPollAt]);
|
|
272
|
-
if (detected._tag === "RepositoryNotModified") {
|
|
273
|
-
if (catalog.stored) {
|
|
274
|
-
yield* writeCursor(watched, nextCursor);
|
|
275
|
-
}
|
|
276
|
-
yield* Effect.logDebug("Repository unchanged (304)", {
|
|
277
|
-
repository: watched.repository.name,
|
|
278
|
-
});
|
|
279
|
-
return {
|
|
280
|
-
changedItems: 0,
|
|
281
|
-
submissions: [],
|
|
282
|
-
revokedRuns: 0,
|
|
283
|
-
failures: catalog.failures,
|
|
284
|
-
nextPollAt,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
yield* Effect.logInfo("Repository changed", {
|
|
288
|
-
repository: watched.repository.name,
|
|
289
|
-
changedItems: detected.items.length,
|
|
290
|
-
backlog: detected.backlog,
|
|
291
|
-
});
|
|
292
|
-
const itemCycles = yield* Effect.forEach(detected.items, (item) => evaluateItem(watched, item), { concurrency: ITEM_CONCURRENCY });
|
|
293
|
-
// Advancing is the commit point: every changed item was fetched,
|
|
294
|
-
// evaluated, and submitted/revoked successfully. A prior failure
|
|
295
|
-
// leaves the old cursor so the next cycle safely re-polls.
|
|
296
|
-
yield* writeCursor(watched, nextCursor);
|
|
297
|
-
return {
|
|
298
|
-
changedItems: detected.items.length,
|
|
299
|
-
submissions: itemCycles.flatMap(({ submissions }) => submissions),
|
|
300
|
-
revokedRuns: itemCycles.reduce((sum, cycle) => sum + cycle.revokedRuns, 0),
|
|
301
|
-
failures: [...catalog.failures, ...itemCycles.flatMap(({ failures }) => failures)],
|
|
302
|
-
nextPollAt,
|
|
303
|
-
};
|
|
304
|
-
});
|
|
305
|
-
const poll = Effect.fn("PollingTriggers.poll")(function* (instance) {
|
|
306
|
-
const paused = new Set((yield* loops.states).filter((state) => state.paused).map((state) => state.loop));
|
|
307
|
-
for (const loop of instance.loops) {
|
|
308
|
-
if (loop.trigger.on !== "schedule" && paused.has(loop.name)) {
|
|
309
|
-
yield* Effect.logDebug("Paused Loop skipped during repository polling", {
|
|
310
|
-
loop: loop.name,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const resolved = (yield* resolveTriggerLoops(instance)).filter(({ loop }) => !paused.has(loop.name));
|
|
315
|
-
const watched = watchedRepositories(instance, resolved);
|
|
316
|
-
const settled = yield* Effect.forEach(watched, (repository) => Effect.result(pollRepository(repository)).pipe(Effect.map((result) => ({ repository, result }))), { concurrency: REPOSITORY_CONCURRENCY });
|
|
317
|
-
const cycles = [];
|
|
318
|
-
const failures = [];
|
|
319
|
-
const backoffs = [];
|
|
320
|
-
for (const { repository, result } of settled) {
|
|
321
|
-
if (Result.isSuccess(result)) {
|
|
322
|
-
cycles.push(result.success);
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const error = result.failure;
|
|
326
|
-
if (error._tag === "GitHubRateLimitError") {
|
|
327
|
-
// A rate-limit pause is scheduling input, not a repository failure.
|
|
328
|
-
yield* Effect.logWarning("GitHub rate limit reached; delaying the next cycle", {
|
|
329
|
-
repository: repository.repository.name,
|
|
330
|
-
operation: error.operation,
|
|
331
|
-
retryAt: DateTime.formatIso(error.retryAt),
|
|
332
|
-
});
|
|
333
|
-
backoffs.push(Option.some(error.retryAt));
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
yield* Effect.logError("Repository poll cycle failed; siblings were unaffected", {
|
|
337
|
-
repository: repository.repository.name,
|
|
338
|
-
...repositoryFailureLogFields(error),
|
|
339
|
-
});
|
|
340
|
-
failures.push(new RepositoryPollFailure({
|
|
341
|
-
repository: repository.repository.name,
|
|
342
|
-
errorTag: error._tag,
|
|
343
|
-
message: error.message,
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
346
|
-
const cycle = new PollCycle({
|
|
347
|
-
repositories: watched.length,
|
|
348
|
-
changedItems: cycles.reduce((sum, cycle) => sum + cycle.changedItems, 0),
|
|
349
|
-
submissions: cycles.flatMap(({ submissions }) => submissions),
|
|
350
|
-
revokedRuns: cycles.reduce((sum, cycle) => sum + cycle.revokedRuns, 0),
|
|
351
|
-
failures: [...cycles.flatMap(({ failures: isolated }) => isolated), ...failures],
|
|
352
|
-
nextPollAt: latestPollAt([...cycles.map(({ nextPollAt }) => nextPollAt), ...backoffs]),
|
|
353
|
-
});
|
|
354
|
-
yield* Ref.set(latest, Option.some(cycle));
|
|
355
|
-
return cycle;
|
|
356
|
-
});
|
|
357
|
-
// No Effect.fn here: `run` never returns, so a named wrapper would open
|
|
358
|
-
// a span that never closes. Each cycle's span lives on `poll`.
|
|
359
|
-
const run = (instance) => Effect.gen(function* () {
|
|
360
|
-
const startedAt = yield* Clock.currentTimeMillis;
|
|
361
|
-
const result = yield* Effect.result(poll(instance));
|
|
362
|
-
if (Result.isFailure(result)) {
|
|
363
|
-
const sleepMs = instance.config.settings.pollInterval * 1_000;
|
|
364
|
-
yield* Effect.logError("Poll cycle could not read Loop runtime state; retrying", {
|
|
365
|
-
errorTag: result.failure._tag,
|
|
366
|
-
message: result.failure.message,
|
|
367
|
-
operation: result.failure.operation,
|
|
368
|
-
sleepMs,
|
|
369
|
-
});
|
|
370
|
-
yield* Effect.sleep(Duration.millis(sleepMs));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
const cycle = result.success;
|
|
374
|
-
const intervalAt = startedAt + instance.config.settings.pollInterval * 1_000;
|
|
375
|
-
const target = Option.match(cycle.nextPollAt, {
|
|
376
|
-
onNone: () => intervalAt,
|
|
377
|
-
onSome: (value) => Math.max(intervalAt, DateTime.toEpochMillis(value)),
|
|
378
|
-
});
|
|
379
|
-
const now = yield* Clock.currentTimeMillis;
|
|
380
|
-
const sleepMs = Math.max(0, target - now);
|
|
381
|
-
yield* Effect.logInfo("Poll cycle complete", {
|
|
382
|
-
repositories: cycle.repositories,
|
|
383
|
-
changedItems: cycle.changedItems,
|
|
384
|
-
submissions: cycle.submissions.length,
|
|
385
|
-
revokedRuns: cycle.revokedRuns,
|
|
386
|
-
failures: cycle.failures.length,
|
|
387
|
-
sleepMs,
|
|
388
|
-
});
|
|
389
|
-
yield* Effect.sleep(Duration.millis(sleepMs));
|
|
390
|
-
}).pipe(Effect.forever);
|
|
391
|
-
return { latest: Ref.get(latest), poll, run };
|
|
392
|
-
}));
|
|
393
|
-
}
|
|
1
|
+
export { PollingTriggers } from "./polling/polling.js";
|
|
2
|
+
export { PollCycle, RepositoryPollFailure } from "./polling/cycle.js";
|
|
3
|
+
export { GitHubItemKey } from "./polling/repository-sync.js";
|
|
394
4
|
//# sourceMappingURL=polling-triggers.js.map
|