@oisincoveney/pipeline 2.8.0 → 2.8.2
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/argo-submit.d.ts +2 -4
- package/dist/argo-submit.js +80 -80
- package/dist/cluster-doctor.js +89 -101
- package/dist/config/defaults.js +9 -19
- package/dist/config/load.js +32 -39
- package/dist/config/schemas.d.ts +1 -1
- package/dist/gates.js +6 -225
- package/dist/mcp/gateway-error.js +15 -0
- package/dist/mcp/gateway.js +119 -220
- package/dist/moka-global-config.js +20 -20
- package/dist/moka-submit.d.ts +1 -1
- package/dist/pipeline-runtime.js +580 -371
- package/dist/run-state/git-refs.js +124 -94
- package/dist/runner-command-contract.d.ts +2 -2
- package/dist/runner-event-sink.js +37 -69
- package/dist/runtime/agent-node/agent-node.js +214 -173
- package/dist/runtime/builtins/builtins.js +344 -57
- package/dist/runtime/changed-files/changed-files.js +15 -27
- package/dist/runtime/changed-files/index.js +2 -0
- package/dist/runtime/drain-merge/drain-merge.js +124 -82
- package/dist/runtime/gates/gates.js +46 -28
- package/dist/runtime/hooks/hooks.js +74 -29
- package/dist/runtime/opencode-server.js +27 -21
- package/dist/runtime/opencode-session-executor.js +101 -44
- package/dist/runtime/parallel-node/parallel-node.js +24 -5
- package/dist/runtime/select-candidate/select-candidate.js +45 -29
- package/dist/runtime/services/agent-node-runtime-service.js +15 -0
- package/dist/runtime/services/command-executor-service.js +8 -0
- package/dist/runtime/services/config-io-service.js +42 -0
- package/dist/runtime/services/drain-merge-git-service.js +10 -0
- package/dist/runtime/services/git-porcelain-service.js +38 -0
- package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
- package/dist/runtime/services/kubernetes-argo-service.js +81 -0
- package/dist/runtime/services/mcp-gateway-service.js +184 -0
- package/dist/runtime/services/opencode-sdk-service.js +27 -0
- package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
- package/dist/runtime/services/select-candidate-service.js +13 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { OpencodeSdkService, OpencodeSdkServiceLive } from "./services/opencode-sdk-service.js";
|
|
1
2
|
import { opencodeAgentName } from "./opencode-agent-name.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
2
4
|
//#region src/runtime/opencode-session-executor.ts
|
|
3
5
|
function createOpencodeSessionRegistry() {
|
|
4
6
|
return { sessions: /* @__PURE__ */ new Map() };
|
|
@@ -18,45 +20,69 @@ const EXIT_INFRA = 70;
|
|
|
18
20
|
*/
|
|
19
21
|
function createOpencodeExecutor(deps) {
|
|
20
22
|
return async function execute(plan, options = {}) {
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
return successResult(plan, await driveSession(deps, plan, options));
|
|
24
|
-
} catch (error) {
|
|
25
|
-
return failureResult(plan, error);
|
|
26
|
-
}
|
|
23
|
+
return await Effect.runPromise(Effect.provide(executeOpencodeEffect(deps, plan, options), OpencodeSdkServiceLive));
|
|
27
24
|
};
|
|
28
25
|
}
|
|
26
|
+
function executeOpencodeEffect(deps, plan, options) {
|
|
27
|
+
return Effect.gen(function* () {
|
|
28
|
+
yield* validateOpencodePlan(plan);
|
|
29
|
+
return yield* executeOpencodeSession(deps, plan, options);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function executeOpencodeSession(deps, plan, options) {
|
|
33
|
+
return Effect.gen(function* () {
|
|
34
|
+
return successResult(plan, yield* driveSession(deps, plan, options));
|
|
35
|
+
}).pipe(Effect.catchAll((error) => Effect.succeed(failureResult(plan, error))));
|
|
36
|
+
}
|
|
37
|
+
function validateOpencodePlan(plan) {
|
|
38
|
+
if (plan.type === "opencode") return Effect.void;
|
|
39
|
+
return Effect.fail(/* @__PURE__ */ new Error(`opencode executor cannot drive runner type '${plan.type}'`));
|
|
40
|
+
}
|
|
29
41
|
function sessionDirectory(deps, plan) {
|
|
30
42
|
return plan.cwd ?? deps.directory;
|
|
31
43
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
function driveSession(deps, plan, options) {
|
|
45
|
+
return Effect.gen(function* () {
|
|
46
|
+
const sessionId = yield* resolveSessionId(deps, plan);
|
|
47
|
+
recordSession(deps, plan.nodeId, sessionId);
|
|
48
|
+
const stream = yield* streamEventsToOutput(deps, sessionId, plan, options);
|
|
49
|
+
return yield* promptSessionResult(deps, plan, sessionId).pipe(Effect.ensuring(stopStream(stream)));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function promptSessionResult(deps, plan, sessionId) {
|
|
53
|
+
return Effect.gen(function* () {
|
|
54
|
+
const data = unwrap(yield* (yield* OpencodeSdkService).promptSession(deps.client, promptRequest(deps, plan, sessionId)));
|
|
42
55
|
return {
|
|
43
56
|
...data.info ? { assistant: data.info } : {},
|
|
44
57
|
parts: data.parts ?? [],
|
|
45
58
|
sessionId
|
|
46
59
|
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function promptRequest(deps, plan, sessionId) {
|
|
63
|
+
return {
|
|
64
|
+
body: promptBody(plan),
|
|
65
|
+
path: { id: sessionId },
|
|
66
|
+
query: { directory: sessionDirectory(deps, plan) }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function stopStream(stream) {
|
|
70
|
+
return Effect.tryPromise(() => stream.stop()).pipe(Effect.asVoid, Effect.catchAll(() => Effect.void));
|
|
50
71
|
}
|
|
51
|
-
|
|
72
|
+
function recordSession(deps, nodeId, sessionId) {
|
|
73
|
+
deps.onSession?.(nodeId, sessionId);
|
|
74
|
+
}
|
|
75
|
+
function resolveSessionId(deps, plan) {
|
|
52
76
|
const existing = deps.registry.sessions.get(plan.nodeId);
|
|
53
|
-
if (existing) return existing;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
if (existing) return Effect.succeed(existing);
|
|
78
|
+
return Effect.gen(function* () {
|
|
79
|
+
const session = unwrap(yield* (yield* OpencodeSdkService).createSession(deps.client, {
|
|
80
|
+
body: { title: `moka:${plan.nodeId}` },
|
|
81
|
+
query: { directory: plan.cwd ?? deps.directory }
|
|
82
|
+
}));
|
|
83
|
+
deps.registry.sessions.set(plan.nodeId, session.id);
|
|
84
|
+
return session.id;
|
|
85
|
+
});
|
|
60
86
|
}
|
|
61
87
|
function promptBody(plan) {
|
|
62
88
|
const prompt = promptText(plan);
|
|
@@ -95,24 +121,55 @@ function parseModel(model) {
|
|
|
95
121
|
providerID: model.slice(0, slash)
|
|
96
122
|
};
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
if (!options.onOutput) return { stop: () => Promise.resolve() };
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
function streamEventsToOutput(deps, sessionId, plan, options) {
|
|
125
|
+
if (!options.onOutput) return Effect.succeed({ stop: () => Promise.resolve() });
|
|
126
|
+
return Effect.gen(function* () {
|
|
127
|
+
const iterator = (yield* (yield* OpencodeSdkService).subscribeEvents(deps.client)).stream;
|
|
128
|
+
const pump = Effect.runPromise(pumpEvents(iterator, sessionId, plan, options));
|
|
129
|
+
return { stop: () => stopIterator(iterator, pump) };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function pumpEvents(iterator, sessionId, plan, options) {
|
|
133
|
+
return Effect.gen(function* () {
|
|
134
|
+
let done = false;
|
|
135
|
+
while (!done) {
|
|
136
|
+
const next = yield* readNextEvent(iterator);
|
|
137
|
+
done = next.done === true;
|
|
138
|
+
if (!done) forwardEvent(next.value, sessionId, plan, options);
|
|
110
139
|
}
|
|
111
|
-
})();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
}).pipe(Effect.catchAll((error) => reportStreamDrop(error, plan, options)));
|
|
141
|
+
}
|
|
142
|
+
function readNextEvent(iterator) {
|
|
143
|
+
return Effect.tryPromise({
|
|
144
|
+
catch: (error) => error,
|
|
145
|
+
try: () => iterator.next()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function reportStreamDrop(error, plan, options) {
|
|
149
|
+
return Effect.sync(() => {
|
|
150
|
+
options.onOutput?.({
|
|
151
|
+
chunk: `opencode event stream dropped: ${errorMessage(error)}\n`,
|
|
152
|
+
nodeId: plan.nodeId,
|
|
153
|
+
stream: "stderr"
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function stopIterator(iterator, pump) {
|
|
158
|
+
return Effect.runPromise(stopIteratorEffect(iterator, pump));
|
|
159
|
+
}
|
|
160
|
+
function stopIteratorEffect(iterator, pump) {
|
|
161
|
+
return Effect.gen(function* () {
|
|
162
|
+
yield* requestIteratorReturn(iterator);
|
|
163
|
+
yield* Effect.tryPromise(() => pump).pipe(Effect.catchAll(() => Effect.void));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function requestIteratorReturn(iterator) {
|
|
167
|
+
const returnIterator = iterator.return;
|
|
168
|
+
if (!returnIterator) return Effect.void;
|
|
169
|
+
return Effect.tryPromise({
|
|
170
|
+
catch: (error) => error,
|
|
171
|
+
try: () => returnIterator.call(iterator, void 0)
|
|
172
|
+
}).pipe(Effect.asVoid, Effect.catchAll(() => Effect.void));
|
|
116
173
|
}
|
|
117
174
|
function forwardEvent(event, sessionId, plan, options) {
|
|
118
175
|
const forwarded = eventChunk(event, sessionId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { childReporter } from "../events/events.js";
|
|
2
2
|
import "../events/index.js";
|
|
3
|
+
import { configUsesOpencode, leaseOpencodeRuntime } from "../opencode-runtime.js";
|
|
3
4
|
import { WorktreeService, WorktreeServiceLive } from "../services/worktree-service.js";
|
|
4
5
|
import { Effect } from "effect";
|
|
5
6
|
//#region src/runtime/parallel-node/parallel-node.ts
|
|
@@ -76,15 +77,33 @@ function gcStaleWorktrees(context) {
|
|
|
76
77
|
* interruption); release retains dirty/unpushed work for downstream selection.
|
|
77
78
|
*/
|
|
78
79
|
function runChildInWorktree(child, context, runtime) {
|
|
79
|
-
if (!context.config.parallel_worktrees?.enabled) return
|
|
80
|
+
if (!context.config.parallel_worktrees?.enabled) return executeChild(child, context, runtime);
|
|
80
81
|
return Effect.gen(function* () {
|
|
81
82
|
const worktree = yield* WorktreeService;
|
|
82
|
-
return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => Effect.
|
|
83
|
-
...context,
|
|
84
|
-
worktreePath: lease.path
|
|
85
|
-
})), (lease) => Effect.sync(() => lease.release()));
|
|
83
|
+
return yield* Effect.acquireUseRelease(worktree.createChild(childLeaseOptions(child, context)), (lease) => runChildWithWorktreeLease(child, context, runtime, lease.path), (lease) => Effect.sync(() => lease.release()));
|
|
86
84
|
});
|
|
87
85
|
}
|
|
86
|
+
function executeChild(child, context, runtime) {
|
|
87
|
+
return Effect.tryPromise(() => runtime.executeNode(child, context)).pipe(Effect.orDie);
|
|
88
|
+
}
|
|
89
|
+
function runChildWithWorktreeLease(child, context, runtime, worktreePath) {
|
|
90
|
+
const childContext = {
|
|
91
|
+
...context,
|
|
92
|
+
worktreePath
|
|
93
|
+
};
|
|
94
|
+
if (!configUsesOpencode(context.config)) return executeChild(child, childContext, runtime);
|
|
95
|
+
return Effect.acquireUseRelease(leaseChildOpencodeRuntime(context, worktreePath), (childRuntime) => executeChild(child, {
|
|
96
|
+
...childContext,
|
|
97
|
+
executor: childRuntime.executor
|
|
98
|
+
}, runtime), (childRuntime) => Effect.tryPromise(() => childRuntime.release()).pipe(Effect.orDie));
|
|
99
|
+
}
|
|
100
|
+
function leaseChildOpencodeRuntime(context, worktreePath) {
|
|
101
|
+
return Effect.tryPromise(() => leaseOpencodeRuntime({
|
|
102
|
+
config: context.config,
|
|
103
|
+
...context.signal ? { signal: context.signal } : {},
|
|
104
|
+
worktreePath
|
|
105
|
+
})).pipe(Effect.orDie);
|
|
106
|
+
}
|
|
88
107
|
function childLeaseOptions(child, context) {
|
|
89
108
|
return {
|
|
90
109
|
childNodeId: child.id,
|
|
@@ -2,7 +2,8 @@ import { createRunnerLaunchPlan } from "../../runner.js";
|
|
|
2
2
|
import { normalizeRunnerOutput } from "../../runner-output.js";
|
|
3
3
|
import { parseJsonObject } from "../json-validation/json-validation.js";
|
|
4
4
|
import "../json-validation/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SelectCandidateService, SelectCandidateServiceLive } from "../services/select-candidate-service.js";
|
|
6
|
+
import { Effect } from "effect";
|
|
6
7
|
//#region src/runtime/select-candidate/select-candidate.ts
|
|
7
8
|
const SCORE_RE = /-?\d+(?:\.\d+)?/;
|
|
8
9
|
function selectBestCandidate(candidates) {
|
|
@@ -11,19 +12,25 @@ function selectBestCandidate(candidates) {
|
|
|
11
12
|
return passing.reduce((best, candidate) => (candidate.judgeScore ?? 0) > (best.judgeScore ?? 0) ? candidate : best);
|
|
12
13
|
}
|
|
13
14
|
async function executeSelectCandidateBuiltin(context, node) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
const program = executeSelectCandidateBuiltinProgram(context, node);
|
|
16
|
+
return await Effect.runPromise(Effect.provide(program, SelectCandidateServiceLive));
|
|
17
|
+
}
|
|
18
|
+
function executeSelectCandidateBuiltinProgram(context, node) {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const candidates = yield* scoreCandidates(context, readCandidates(context, firstNeed(node)));
|
|
21
|
+
const selected = selectBestCandidate(candidates);
|
|
22
|
+
if (!selected) return {
|
|
23
|
+
evidence: [`select-candidate: no passing candidate among ${candidates.length}`, ...candidates.map((candidate) => `- ${candidate.nodeId}: FAIL`)],
|
|
24
|
+
exitCode: 1,
|
|
25
|
+
output: ""
|
|
26
|
+
};
|
|
27
|
+
const promoted = yield* promoteWinner(context, node, selected.nodeId);
|
|
28
|
+
return {
|
|
29
|
+
evidence: selectionEvidence(selected, candidates.length, promoted),
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
output: selected.output
|
|
32
|
+
};
|
|
33
|
+
});
|
|
27
34
|
}
|
|
28
35
|
function selectionEvidence(selected, candidateCount, promoted) {
|
|
29
36
|
const lines = [`select-candidate: selected '${selected.nodeId}' (judge=${selected.judgeScore ?? "n/a"}) from ${candidateCount} candidates`];
|
|
@@ -31,24 +38,33 @@ function selectionEvidence(selected, candidateCount, promoted) {
|
|
|
31
38
|
return lines;
|
|
32
39
|
}
|
|
33
40
|
function promoteWinner(context, node, winnerNodeId) {
|
|
34
|
-
const parentNodeId = node
|
|
35
|
-
if (!(context
|
|
36
|
-
return
|
|
41
|
+
const parentNodeId = firstNeed(node);
|
|
42
|
+
if (!shouldPromoteWinner(context, parentNodeId)) return Effect.succeed([]);
|
|
43
|
+
return SelectCandidateService.pipe(Effect.flatMap((service) => service.promoteWinner(context.worktreePath, context.runId, parentNodeId, winnerNodeId)));
|
|
37
44
|
}
|
|
38
|
-
|
|
45
|
+
function shouldPromoteWinner(context, parentNodeId) {
|
|
46
|
+
const parallelWorktrees = context.config.parallel_worktrees;
|
|
47
|
+
return Boolean(parallelWorktrees?.enabled) && parentNodeId !== null;
|
|
48
|
+
}
|
|
49
|
+
function firstNeed(node) {
|
|
50
|
+
return node?.needs.at(0) ?? null;
|
|
51
|
+
}
|
|
52
|
+
function scoreCandidates(context, candidates) {
|
|
39
53
|
const model = context.config.best_of_n?.judge_model;
|
|
40
54
|
const runner = Object.keys(context.config.runners).at(0);
|
|
41
|
-
if (!(model && runner)) return candidates;
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
if (!(model && runner)) return Effect.succeed(candidates);
|
|
56
|
+
return Effect.forEach(candidates, (candidate) => scoreCandidate(context, candidate, runner, model), { concurrency: "unbounded" });
|
|
57
|
+
}
|
|
58
|
+
function scoreCandidate(context, candidate, runner, model) {
|
|
59
|
+
return Effect.gen(function* () {
|
|
60
|
+
const plan = judgePlan(context, candidate, runner, model);
|
|
61
|
+
context.agentInvocations.push(plan);
|
|
62
|
+
const judgeScore = parseScore(normalizeRunnerOutput(plan, (yield* (yield* SelectCandidateService).executeRunner(context.executor, plan, { signal: context.signal })).stdout).output);
|
|
63
|
+
return judgeScore === null ? candidate : {
|
|
64
|
+
...candidate,
|
|
65
|
+
judgeScore
|
|
66
|
+
};
|
|
67
|
+
});
|
|
52
68
|
}
|
|
53
69
|
function judgePlan(context, candidate, runner, model) {
|
|
54
70
|
const profileId = `select-candidate:judge:${candidate.nodeId}`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildRepoMapContext } from "../../context/repo-map.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
//#region src/runtime/services/agent-node-runtime-service.ts
|
|
5
|
+
var AgentNodeRuntimeService = class extends Context.Tag("AgentNodeRuntimeService")() {};
|
|
6
|
+
const AgentNodeRuntimeServiceLive = Layer.succeed(AgentNodeRuntimeService, {
|
|
7
|
+
buildRepoMap: buildRepoMapContext,
|
|
8
|
+
executeRunner: (executor, plan, options) => Effect.tryPromise({
|
|
9
|
+
catch: (error) => error,
|
|
10
|
+
try: () => Promise.resolve(executor(plan, options))
|
|
11
|
+
}),
|
|
12
|
+
readText: (path) => Effect.sync(() => readFileSync(path, "utf8"))
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { AgentNodeRuntimeService, AgentNodeRuntimeServiceLive };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { executeCommand } from "../command-executor/command-executor.js";
|
|
2
|
+
import "../command-executor/index.js";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
//#region src/runtime/services/command-executor-service.ts
|
|
5
|
+
var CommandExecutor = class extends Context.Tag("CommandExecutor")() {};
|
|
6
|
+
const CommandExecutorLive = Layer.succeed(CommandExecutor, { execute: (command, context, options) => Effect.tryPromise(() => executeCommand(command, context, options)) });
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CommandExecutor, CommandExecutorLive };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PipelineConfigError, configIssuesFromZodError, validationError } from "../../config/schemas.js";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { Cause, Context, Effect, Layer, Option } from "effect";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
//#region src/runtime/services/config-io-service.ts
|
|
6
|
+
function parseYamlSource(source, sourcePath) {
|
|
7
|
+
const document = parseDocument(source, {
|
|
8
|
+
prettyErrors: false,
|
|
9
|
+
uniqueKeys: true
|
|
10
|
+
});
|
|
11
|
+
if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
|
|
12
|
+
message: err.message,
|
|
13
|
+
path: sourcePath
|
|
14
|
+
})));
|
|
15
|
+
return document.toJS();
|
|
16
|
+
}
|
|
17
|
+
var ConfigIoService = class extends Context.Tag("ConfigIoService")() {};
|
|
18
|
+
const ConfigIoServiceLive = Layer.succeed(ConfigIoService, {
|
|
19
|
+
parseYaml: (source, sourcePath) => Effect.try({
|
|
20
|
+
try: () => parseYamlSource(source, sourcePath),
|
|
21
|
+
catch: (error) => error
|
|
22
|
+
}),
|
|
23
|
+
readOptionalText: (path) => Effect.sync(() => existsSync(path) ? readFileSync(path, "utf8") : null),
|
|
24
|
+
readText: (path) => Effect.sync(() => readFileSync(path, "utf8"))
|
|
25
|
+
});
|
|
26
|
+
function parseConfigYamlAs(source, sourcePath, schema) {
|
|
27
|
+
return Effect.gen(function* () {
|
|
28
|
+
const yaml = yield* (yield* ConfigIoService).parseYaml(source, sourcePath);
|
|
29
|
+
const parsed = schema.safeParse(yaml);
|
|
30
|
+
if (!parsed.success) return yield* Effect.fail(validationError(configIssuesFromZodError(parsed.error)));
|
|
31
|
+
return parsed.data;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function runConfigIoSync(program) {
|
|
35
|
+
const exit = Effect.runSyncExit(Effect.provide(program, ConfigIoServiceLive));
|
|
36
|
+
if (exit._tag === "Success") return exit.value;
|
|
37
|
+
const failure = Cause.failureOption(exit.cause);
|
|
38
|
+
if (Option.isSome(failure)) throw failure.value;
|
|
39
|
+
throw Cause.squash(exit.cause);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ConfigIoService, parseConfigYamlAs, runConfigIoSync };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import simpleGit$1 from "simple-git";
|
|
3
|
+
//#region src/runtime/services/drain-merge-git-service.ts
|
|
4
|
+
var DrainMergeGitService = class extends Context.Tag("DrainMergeGitService")() {};
|
|
5
|
+
const DrainMergeGitServiceLive = Layer.succeed(DrainMergeGitService, { create: (baseDir) => Effect.sync(() => {
|
|
6
|
+
const git = simpleGit$1({ baseDir });
|
|
7
|
+
return { raw: (args) => Effect.tryPromise(() => git.raw(args)) };
|
|
8
|
+
}) });
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DrainMergeGitService, DrainMergeGitServiceLive };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
//#region src/runtime/services/git-porcelain-service.ts
|
|
5
|
+
const execGit = promisify(execFile);
|
|
6
|
+
var GitPorcelainService = class extends Context.Tag("GitPorcelainService")() {};
|
|
7
|
+
const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
|
|
8
|
+
run: (cwd, args, env) => Effect.tryPromise({
|
|
9
|
+
try: async () => {
|
|
10
|
+
const { stdout } = await execGit("git", args, {
|
|
11
|
+
cwd,
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
env
|
|
14
|
+
});
|
|
15
|
+
return stdout;
|
|
16
|
+
},
|
|
17
|
+
catch: (error) => error
|
|
18
|
+
}),
|
|
19
|
+
statusPorcelain: (cwd) => Effect.try({
|
|
20
|
+
try: () => execFileSync("git", [
|
|
21
|
+
"status",
|
|
22
|
+
"--porcelain=v1",
|
|
23
|
+
"--untracked-files=all",
|
|
24
|
+
"-z"
|
|
25
|
+
], {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
stdio: [
|
|
29
|
+
"ignore",
|
|
30
|
+
"pipe",
|
|
31
|
+
"ignore"
|
|
32
|
+
]
|
|
33
|
+
}),
|
|
34
|
+
catch: (error) => error
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
export { GitPorcelainService, GitPorcelainServiceLive };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
3
|
+
|
|
4
|
+
//#region src/runtime/services/kubernetes-argo-service.d.ts
|
|
5
|
+
type CoreApi = Pick<CoreV1Api, "createNamespacedConfigMap">;
|
|
6
|
+
type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject">;
|
|
7
|
+
interface KubernetesArgoIoDependencies {
|
|
8
|
+
coreApi?: CoreApi;
|
|
9
|
+
kubeConfig?: KubeConfig;
|
|
10
|
+
workflowApi?: WorkflowApi;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CoreApi, KubernetesArgoIoDependencies, WorkflowApi };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
4
|
+
//#region src/runtime/services/kubernetes-argo-service.ts
|
|
5
|
+
var KubernetesArgoService = class extends Context.Tag("KubernetesArgoService")() {};
|
|
6
|
+
const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
|
|
7
|
+
createConfigMap: ({ body, dependencies, namespace, options }) => Effect.try({
|
|
8
|
+
try: () => apiClients(options, dependencies),
|
|
9
|
+
catch: (error) => error
|
|
10
|
+
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
11
|
+
try: () => coreApi.createNamespacedConfigMap({
|
|
12
|
+
body,
|
|
13
|
+
namespace
|
|
14
|
+
}),
|
|
15
|
+
catch: (error) => error
|
|
16
|
+
}))),
|
|
17
|
+
createWorkflow: ({ body, dependencies, namespace, options }) => Effect.try({
|
|
18
|
+
try: () => apiClients(options, dependencies),
|
|
19
|
+
catch: (error) => error
|
|
20
|
+
}).pipe(Effect.flatMap(({ workflowApi }) => Effect.tryPromise({
|
|
21
|
+
try: () => workflowApi.createNamespacedCustomObject({
|
|
22
|
+
body,
|
|
23
|
+
group: "argoproj.io",
|
|
24
|
+
namespace,
|
|
25
|
+
plural: "workflows",
|
|
26
|
+
version: "v1alpha1"
|
|
27
|
+
}),
|
|
28
|
+
catch: (error) => error
|
|
29
|
+
}))),
|
|
30
|
+
kubectl: (args, options) => Effect.tryPromise({
|
|
31
|
+
try: () => execa("kubectl", kubectlArgs(args, options.kubeContext), {
|
|
32
|
+
env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
|
|
33
|
+
stdin: "ignore"
|
|
34
|
+
}),
|
|
35
|
+
catch: (error) => error
|
|
36
|
+
}).pipe(Effect.map((result) => ({
|
|
37
|
+
ok: true,
|
|
38
|
+
stderr: result.stderr,
|
|
39
|
+
stdout: result.stdout
|
|
40
|
+
})), Effect.catchAll((error) => Effect.succeed({
|
|
41
|
+
ok: false,
|
|
42
|
+
stderr: kubectlErrorStderr(error),
|
|
43
|
+
stdout: kubectlErrorStdout(error)
|
|
44
|
+
})))
|
|
45
|
+
});
|
|
46
|
+
function resolveKubeConfig(options, dependencies) {
|
|
47
|
+
if (dependencies.kubeConfig) return dependencies.kubeConfig;
|
|
48
|
+
const kubeConfig = new KubeConfig();
|
|
49
|
+
if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
|
|
50
|
+
else kubeConfig.loadFromDefault();
|
|
51
|
+
return kubeConfig;
|
|
52
|
+
}
|
|
53
|
+
function apiClients(options, dependencies) {
|
|
54
|
+
if (dependencies.coreApi && dependencies.workflowApi) return {
|
|
55
|
+
coreApi: dependencies.coreApi,
|
|
56
|
+
workflowApi: dependencies.workflowApi
|
|
57
|
+
};
|
|
58
|
+
return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
|
|
59
|
+
}
|
|
60
|
+
function buildApiClients(kubeConfig, dependencies) {
|
|
61
|
+
return {
|
|
62
|
+
coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
|
|
63
|
+
workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function kubectlArgs(args, kubeContext) {
|
|
67
|
+
return kubeContext ? [
|
|
68
|
+
"--context",
|
|
69
|
+
kubeContext,
|
|
70
|
+
...args
|
|
71
|
+
] : [...args];
|
|
72
|
+
}
|
|
73
|
+
function kubectlErrorStderr(error) {
|
|
74
|
+
const parsed = error;
|
|
75
|
+
return (parsed.stderr || parsed.shortMessage || "kubectl failed").trim();
|
|
76
|
+
}
|
|
77
|
+
function kubectlErrorStdout(error) {
|
|
78
|
+
return (error.stdout || "").trim();
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { KubernetesArgoService, KubernetesArgoServiceLive };
|