@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
|
@@ -2,12 +2,16 @@ import { parseJsonObject } from "../json-validation/json-validation.js";
|
|
|
2
2
|
import "../json-validation/index.js";
|
|
3
3
|
import { generateRuntimeRunId } from "../context/context.js";
|
|
4
4
|
import "../context/index.js";
|
|
5
|
-
import
|
|
5
|
+
import { DrainMergeGitService, DrainMergeGitServiceLive } from "../services/drain-merge-git-service.js";
|
|
6
|
+
import { Effect } from "effect";
|
|
6
7
|
//#region src/runtime/drain-merge/drain-merge.ts
|
|
7
8
|
const LINE_RE = /\r?\n/;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function executeDrainMergeBuiltin(context, node) {
|
|
10
|
+
return Effect.runPromise(Effect.provide(drainMergeProgram(context, node), DrainMergeGitServiceLive));
|
|
11
|
+
}
|
|
12
|
+
function drainMergeProgram(context, node) {
|
|
13
|
+
const upstreamNodeId = firstNeededNode(node);
|
|
14
|
+
const integrationBranch = `runs/integration/${runIdForDrainMerge(context)}`;
|
|
11
15
|
const report = {
|
|
12
16
|
baseSha: null,
|
|
13
17
|
conflicts: [],
|
|
@@ -16,47 +20,31 @@ async function executeDrainMergeBuiltin(context, node) {
|
|
|
16
20
|
skipped: []
|
|
17
21
|
};
|
|
18
22
|
const mergeable = drainMergeMergeableChildren(drainMergeChildren(context, upstreamNodeId), report);
|
|
19
|
-
if (mergeable.length === 0) return drainMergeResult(report);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
if (mergeable.length === 0) return Effect.succeed(drainMergeResult(report));
|
|
24
|
+
const baseSha = mergeable[0].output.baseSha;
|
|
25
|
+
report.baseSha = baseSha;
|
|
26
|
+
const divergent = divergentDrainMergeChild(mergeable, baseSha);
|
|
27
|
+
if (divergent) return Effect.succeed(drainMergeResult(report, {
|
|
23
28
|
evidence: [`drain-merge child '${divergent.nodeId}' baseSha ${divergent.output.baseSha} diverges from ${report.baseSha}`],
|
|
24
29
|
failed: true
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
for (const child of mergeable) try {
|
|
36
|
-
await git.raw([
|
|
37
|
-
"merge",
|
|
38
|
-
"--no-ff",
|
|
39
|
-
"--no-edit",
|
|
40
|
-
"-m",
|
|
41
|
-
"drain-merge: merge",
|
|
42
|
-
child.output.branch
|
|
43
|
-
]);
|
|
44
|
-
report.merged.push({
|
|
45
|
-
branch: child.output.branch,
|
|
46
|
-
id: child.nodeId,
|
|
47
|
-
worktreePath: child.output.worktreePath
|
|
30
|
+
}));
|
|
31
|
+
return Effect.gen(function* () {
|
|
32
|
+
const git = yield* (yield* DrainMergeGitService).create(context.worktreePath);
|
|
33
|
+
const setup = checkoutDrainMergeIntegrationBranch(git, integrationBranch, baseSha);
|
|
34
|
+
const setupResult = yield* Effect.either(setup);
|
|
35
|
+
if (setupResult._tag === "Left") return drainMergeSetupErrorResult(report, setupResult.left);
|
|
36
|
+
yield* Effect.forEach(mergeable, (child) => mergeDrainMergeChild(git, report, child), {
|
|
37
|
+
concurrency: 1,
|
|
38
|
+
discard: true
|
|
48
39
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await abortDrainMerge(git);
|
|
58
|
-
}
|
|
59
|
-
return drainMergeResult(report);
|
|
40
|
+
return drainMergeResult(report);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function firstNeededNode(node) {
|
|
44
|
+
return node?.needs.at(0) ?? null;
|
|
45
|
+
}
|
|
46
|
+
function runIdForDrainMerge(context) {
|
|
47
|
+
return context.runId ?? generateRuntimeRunId();
|
|
60
48
|
}
|
|
61
49
|
function drainMergeChildren(context, upstreamNodeId) {
|
|
62
50
|
if (!upstreamNodeId) return [];
|
|
@@ -80,7 +68,7 @@ function drainMergeMergeableChildren(children, report) {
|
|
|
80
68
|
});
|
|
81
69
|
return [];
|
|
82
70
|
}
|
|
83
|
-
if (!(child.output
|
|
71
|
+
if (!hasDrainMergeWorktree(child.output)) {
|
|
84
72
|
report.skipped.push({
|
|
85
73
|
id: child.nodeId,
|
|
86
74
|
reason: "no-worktree",
|
|
@@ -99,56 +87,110 @@ function drainMergeMergeableChildren(children, report) {
|
|
|
99
87
|
}];
|
|
100
88
|
});
|
|
101
89
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
90
|
+
function hasDrainMergeWorktree(output) {
|
|
91
|
+
return Boolean(output.baseSha) && Boolean(output.branch) && Boolean(output.worktreePath);
|
|
92
|
+
}
|
|
93
|
+
function divergentDrainMergeChild(mergeable, baseSha) {
|
|
94
|
+
return mergeable.find((child) => child.output.baseSha !== baseSha);
|
|
95
|
+
}
|
|
96
|
+
function checkoutDrainMergeIntegrationBranch(git, integrationBranch, baseSha) {
|
|
97
|
+
return git.raw([
|
|
98
|
+
"rev-parse",
|
|
99
|
+
"--verify",
|
|
100
|
+
integrationBranch
|
|
101
|
+
]).pipe(Effect.flatMap(() => git.raw(["checkout", integrationBranch])), Effect.catchAll(() => git.raw([
|
|
102
|
+
"checkout",
|
|
103
|
+
"-b",
|
|
104
|
+
integrationBranch,
|
|
105
|
+
baseSha
|
|
106
|
+
])), Effect.asVoid);
|
|
107
|
+
}
|
|
108
|
+
function mergeDrainMergeChild(git, report, child) {
|
|
109
|
+
return drainMergeChild(git, child.output.branch).pipe(Effect.tap(() => Effect.sync(() => recordDrainMergeSuccess(report, child))), Effect.catchAll(() => recordDrainMergeConflict(git, report, child)));
|
|
110
|
+
}
|
|
111
|
+
function drainMergeChild(git, branch) {
|
|
112
|
+
return git.raw([
|
|
113
|
+
"merge",
|
|
114
|
+
"--no-ff",
|
|
115
|
+
"--no-edit",
|
|
116
|
+
"-m",
|
|
117
|
+
"drain-merge: merge",
|
|
118
|
+
branch
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
function recordDrainMergeSuccess(report, child) {
|
|
122
|
+
report.merged.push({
|
|
123
|
+
branch: child.output.branch,
|
|
124
|
+
id: child.nodeId,
|
|
125
|
+
worktreePath: child.output.worktreePath
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function recordDrainMergeConflict(git, report, child) {
|
|
129
|
+
return Effect.gen(function* () {
|
|
130
|
+
const files = yield* drainMergeConflictFiles(git);
|
|
131
|
+
report.conflicts.push({
|
|
132
|
+
branch: child.output.branch,
|
|
133
|
+
files,
|
|
134
|
+
id: child.nodeId,
|
|
135
|
+
worktreePath: child.output.worktreePath
|
|
136
|
+
});
|
|
137
|
+
yield* abortDrainMerge(git);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function drainMergeConflictFiles(git) {
|
|
141
|
+
return git.raw([
|
|
142
|
+
"diff",
|
|
143
|
+
"--name-only",
|
|
144
|
+
"--diff-filter=U"
|
|
145
|
+
]).pipe(Effect.map((output) => output.split(LINE_RE).filter(Boolean)), Effect.catchAll(() => Effect.succeed([])));
|
|
146
|
+
}
|
|
147
|
+
function abortDrainMerge(git) {
|
|
148
|
+
return git.raw(["merge", "--abort"]).pipe(Effect.catchAll(() => Effect.void), Effect.asVoid);
|
|
134
149
|
}
|
|
135
150
|
function parseDrainMergeChildOutput(value) {
|
|
136
151
|
const output = parseJsonObject(value);
|
|
137
152
|
if (Object.keys(output).length === 0) return null;
|
|
138
153
|
return {
|
|
139
|
-
baseSha:
|
|
140
|
-
branch:
|
|
141
|
-
status: output.status
|
|
142
|
-
worktreePath:
|
|
154
|
+
baseSha: stringFieldOrNull(output.baseSha),
|
|
155
|
+
branch: stringFieldOrNull(output.branch),
|
|
156
|
+
status: drainMergeStatus(output.status),
|
|
157
|
+
worktreePath: stringFieldOrNull(output.worktreePath)
|
|
143
158
|
};
|
|
144
159
|
}
|
|
160
|
+
function stringFieldOrNull(value) {
|
|
161
|
+
return typeof value === "string" ? value : null;
|
|
162
|
+
}
|
|
163
|
+
function drainMergeStatus(value) {
|
|
164
|
+
return value === "PASS" ? "PASS" : "FAIL";
|
|
165
|
+
}
|
|
145
166
|
function drainMergeResult(report, options = {}) {
|
|
146
|
-
const hasFailure = report
|
|
167
|
+
const hasFailure = hasDrainMergeFailure(report, options.failed);
|
|
147
168
|
return {
|
|
148
|
-
evidence:
|
|
149
|
-
exitCode: hasFailure
|
|
169
|
+
evidence: drainMergeEvidence(report, options.evidence, hasFailure),
|
|
170
|
+
exitCode: drainMergeExitCode(hasFailure),
|
|
150
171
|
output: JSON.stringify(report)
|
|
151
172
|
};
|
|
152
173
|
}
|
|
174
|
+
function hasDrainMergeFailure(report, failed) {
|
|
175
|
+
return report.conflicts.length > 0 || failed === true;
|
|
176
|
+
}
|
|
177
|
+
function drainMergeEvidence(report, evidence, hasFailure) {
|
|
178
|
+
return [...evidence ?? [], drainMergeSummary(report, hasFailure)];
|
|
179
|
+
}
|
|
180
|
+
function drainMergeSummary(report, hasFailure) {
|
|
181
|
+
return hasFailure ? `drain-merge completed with ${report.conflicts.length} conflicts` : `drain-merge merged ${report.merged.length} branches`;
|
|
182
|
+
}
|
|
183
|
+
function drainMergeExitCode(hasFailure) {
|
|
184
|
+
return hasFailure ? 1 : 0;
|
|
185
|
+
}
|
|
186
|
+
function drainMergeSetupErrorResult(report, error) {
|
|
187
|
+
return drainMergeResult(report, {
|
|
188
|
+
evidence: [`drain-merge setup-error: ${errorMessage(error)}`],
|
|
189
|
+
failed: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function errorMessage(error) {
|
|
193
|
+
return error instanceof Error ? error.message : String(error);
|
|
194
|
+
}
|
|
153
195
|
//#endregion
|
|
154
196
|
export { executeDrainMergeBuiltin };
|
|
@@ -1,46 +1,64 @@
|
|
|
1
1
|
import { isRecord, parseJsonResult } from "../../safe-json.js";
|
|
2
2
|
import { runtimeActorId } from "../actor-ids.js";
|
|
3
|
-
import { executeCommand } from "../command-executor/command-executor.js";
|
|
4
|
-
import "../command-executor/index.js";
|
|
5
3
|
import { readOptionalFile, validateJsonSchemaSource } from "../json-validation/json-validation.js";
|
|
6
4
|
import "../json-validation/index.js";
|
|
7
5
|
import { emitGateFinish, emitGateStart, runtimeSystemId } from "../events/events.js";
|
|
8
6
|
import "../events/index.js";
|
|
9
|
-
import {
|
|
7
|
+
import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
|
|
10
8
|
import { executeBuiltin } from "../builtins/builtins.js";
|
|
11
9
|
import "../builtins/index.js";
|
|
10
|
+
import { artifactExists } from "../../gates.js";
|
|
11
|
+
import { Effect } from "effect";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import micromatch from "micromatch";
|
|
14
14
|
//#region src/runtime/gates/gates.ts
|
|
15
|
-
|
|
15
|
+
function evaluateNodeGates(node, context, attempt, onGateFailure) {
|
|
16
|
+
return Effect.runPromise(Effect.provide(evaluateNodeGatesEffect(node, context, attempt, onGateFailure), CommandExecutorLive));
|
|
17
|
+
}
|
|
18
|
+
function evaluateNodeGatesEffect(node, context, attempt, onGateFailure) {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const executor = yield* CommandExecutor;
|
|
21
|
+
return yield* Effect.tryPromise(() => evaluateNodeGatesWithExecutor(node, context, attempt, executor, onGateFailure));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function evaluateNodeGatesWithExecutor(node, context, attempt, executor, onGateFailure) {
|
|
16
25
|
const results = [];
|
|
17
|
-
for (const gate of nodeGateSpecs(node, context))
|
|
18
|
-
const gateId = gate.id ?? `${gate.kind}:${node.id}`;
|
|
19
|
-
if (isCancelled(context)) {
|
|
20
|
-
emitRuntimeGateCancelled(context, gate, gateId, node.id, "gate cancelled");
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
emitGateStart(context, node.id, gate, gateId);
|
|
24
|
-
const result = await runGateEvaluation(gate, gateId, node.id, context, attempt);
|
|
25
|
-
context.gates.push(result);
|
|
26
|
-
results.push(result);
|
|
27
|
-
emitGateFinish(context, gate, result);
|
|
28
|
-
if (!result.passed) {
|
|
29
|
-
await onGateFailure?.(node, result);
|
|
30
|
-
if (gate.required !== false) break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
26
|
+
for (const gate of nodeGateSpecs(node, context)) if (await evaluateNodeGateIteration(gate, node, context, attempt, executor, results, onGateFailure) === "stop") break;
|
|
33
27
|
return results;
|
|
34
28
|
}
|
|
35
|
-
async function
|
|
29
|
+
async function evaluateNodeGateIteration(gate, node, context, attempt, executor, results, onGateFailure) {
|
|
30
|
+
const gateId = gate.id ?? `${gate.kind}:${node.id}`;
|
|
31
|
+
if (isCancelled(context)) {
|
|
32
|
+
emitRuntimeGateCancelled(context, gate, gateId, node.id, "gate cancelled");
|
|
33
|
+
return "stop";
|
|
34
|
+
}
|
|
35
|
+
const result = await runObservedGate(gate, gateId, node.id, context, attempt, executor);
|
|
36
|
+
recordGateResult(context, gate, result, results);
|
|
37
|
+
return handleGateFailure(gate, node, result, onGateFailure);
|
|
38
|
+
}
|
|
39
|
+
function runObservedGate(gate, gateId, nodeId, context, attempt, executor) {
|
|
40
|
+
emitGateStart(context, nodeId, gate, gateId);
|
|
41
|
+
return runGateEvaluation(gate, gateId, nodeId, context, attempt, executor);
|
|
42
|
+
}
|
|
43
|
+
function recordGateResult(context, gate, result, results) {
|
|
44
|
+
context.gates.push(result);
|
|
45
|
+
results.push(result);
|
|
46
|
+
emitGateFinish(context, gate, result);
|
|
47
|
+
}
|
|
48
|
+
async function handleGateFailure(gate, node, result, onGateFailure) {
|
|
49
|
+
if (result.passed) return "continue";
|
|
50
|
+
if (onGateFailure) await onGateFailure(node, result);
|
|
51
|
+
return gate.required === false ? "continue" : "stop";
|
|
52
|
+
}
|
|
53
|
+
async function runGateEvaluation(gate, gateId, nodeId, context, attempt, executor) {
|
|
36
54
|
emitRuntimeGateStarted(context, gate, gateId, nodeId);
|
|
37
|
-
const result = await resolveGateResult(gate, gateId, nodeId, context, attempt);
|
|
55
|
+
const result = await resolveGateResult(gate, gateId, nodeId, context, attempt, executor);
|
|
38
56
|
emitRuntimeGateResult(context, result);
|
|
39
57
|
return result;
|
|
40
58
|
}
|
|
41
|
-
async function resolveGateResult(gate, gateId, nodeId, context, attempt) {
|
|
59
|
+
async function resolveGateResult(gate, gateId, nodeId, context, attempt, executor) {
|
|
42
60
|
try {
|
|
43
|
-
return await evaluateGate(gate, nodeId, context, attempt);
|
|
61
|
+
return await evaluateGate(gate, nodeId, context, attempt, executor);
|
|
44
62
|
} catch (err) {
|
|
45
63
|
return {
|
|
46
64
|
evidence: [err instanceof Error ? err.message : String(err)],
|
|
@@ -135,11 +153,11 @@ function schemaGateSpecs(node, context) {
|
|
|
135
153
|
target: "stdout"
|
|
136
154
|
}];
|
|
137
155
|
}
|
|
138
|
-
function evaluateGate(gate, nodeId, context, attempt) {
|
|
156
|
+
function evaluateGate(gate, nodeId, context, attempt, executor) {
|
|
139
157
|
const gateId = gate.id ?? `${gate.kind}:${nodeId}`;
|
|
140
158
|
const node = context.plan.graph.node(nodeId);
|
|
141
159
|
switch (gate.kind) {
|
|
142
|
-
case "command": return evaluateCommandGate(gate, gateId, nodeId, context);
|
|
160
|
+
case "command": return evaluateCommandGate(gate, gateId, nodeId, context, executor);
|
|
143
161
|
case "artifact": return evaluateArtifactGate(gate, gateId, nodeId, context);
|
|
144
162
|
case "builtin": return evaluateBuiltinGate(gate, gateId, nodeId, context);
|
|
145
163
|
case "verdict": return evaluateVerdictGate(gate, gateId, nodeId, context, attempt);
|
|
@@ -152,8 +170,8 @@ function evaluateGate(gate, nodeId, context, attempt) {
|
|
|
152
170
|
function assertNever(value) {
|
|
153
171
|
throw new Error(`Unsupported gate kind: ${String(value)}`);
|
|
154
172
|
}
|
|
155
|
-
async function evaluateCommandGate(gate, gateId, nodeId, context) {
|
|
156
|
-
const result = await
|
|
173
|
+
async function evaluateCommandGate(gate, gateId, nodeId, context, executor) {
|
|
174
|
+
const result = await Effect.runPromise(executor.execute(gate.command ?? [], context, { timeout: gate.timeout_ms }));
|
|
157
175
|
const expected = gate.expect_exit_code ?? 0;
|
|
158
176
|
return {
|
|
159
177
|
evidence: result.evidence,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parseJson } from "../../safe-json.js";
|
|
2
2
|
import { parseHookResult } from "../../hooks.js";
|
|
3
3
|
import { runtimeActorId } from "../actor-ids.js";
|
|
4
|
-
import { executeCommand } from "../command-executor/command-executor.js";
|
|
5
|
-
import "../command-executor/index.js";
|
|
6
4
|
import { validateJsonSchemaSource } from "../json-validation/json-validation.js";
|
|
7
5
|
import "../json-validation/index.js";
|
|
8
6
|
import { emit, runtimeSystemId } from "../events/events.js";
|
|
9
7
|
import "../events/index.js";
|
|
8
|
+
import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
|
|
9
|
+
import { Effect } from "effect";
|
|
10
10
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join, resolve } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
@@ -225,34 +225,79 @@ function hookModuleSpecifier(hookFunction, context) {
|
|
|
225
225
|
if (hookFunction.module.startsWith(".") || hookFunction.module.startsWith("/")) return pathToFileURL(resolve(context.worktreePath, hookFunction.module)).href;
|
|
226
226
|
return hookFunction.module;
|
|
227
227
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
228
|
+
function executeCommandHookFunction(hookFunction, binding, event, context, failure, node, gateId) {
|
|
229
|
+
const program = executeCommandHookFunctionEffect(hookFunction, binding, event, context, failure, node, gateId);
|
|
230
|
+
return Effect.runPromise(Effect.provide(program, CommandExecutorLive));
|
|
231
|
+
}
|
|
232
|
+
function executeCommandHookFunctionEffect(hookFunction, binding, event, context, failure, node, gateId) {
|
|
233
|
+
return Effect.gen(function* () {
|
|
234
|
+
const policyFailure = commandHookPolicyFailure(hookFunction, binding, context, node);
|
|
235
|
+
if (policyFailure) return { failure: policyFailure };
|
|
236
|
+
const files = yield* createHookTempFiles();
|
|
237
|
+
return yield* runCommandHookWithTempFiles(files, hookFunction, binding, event, context, failure, node, gateId).pipe(Effect.ensuring(removeHookTempDir(files.tempDir)));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function createHookTempFiles() {
|
|
241
|
+
return Effect.try(() => {
|
|
242
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pipeline-hook-"));
|
|
243
|
+
return {
|
|
244
|
+
inputPath: join(tempDir, "input.json"),
|
|
245
|
+
resultPath: join(tempDir, "result.json"),
|
|
246
|
+
tempDir
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function removeHookTempDir(tempDir) {
|
|
251
|
+
return Effect.sync(() => rmSync(tempDir, {
|
|
252
|
+
force: true,
|
|
253
|
+
recursive: true
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function commandHookPolicyFailure(hookFunction, binding, context, node) {
|
|
257
|
+
if (commandHooksDisabled(context)) return commandHookFailure(binding, "command hooks are disabled", node);
|
|
258
|
+
if (untrustedCommandHookDisabled(hookFunction, context)) return commandHookFailure(binding, "command hook is not trusted", node);
|
|
259
|
+
}
|
|
260
|
+
function commandHooksDisabled(context) {
|
|
261
|
+
return context.hookPolicy.allowCommandHooks === false || context.config.hooks.policy?.commands === "deny";
|
|
262
|
+
}
|
|
263
|
+
function untrustedCommandHookDisabled(hookFunction, context) {
|
|
264
|
+
const commandPolicy = context.config.hooks.policy?.commands;
|
|
265
|
+
return hookFunction.trusted !== true && (commandPolicy === "trusted-only" || context.hookPolicy.allowUntrustedCommandHooks === false);
|
|
266
|
+
}
|
|
267
|
+
function commandHookFailure(binding, evidence, node) {
|
|
268
|
+
return runtimeHookFailure(binding, `hook '${binding.id}' failed`, [evidence], node);
|
|
269
|
+
}
|
|
270
|
+
function runCommandHookWithTempFiles(files, hookFunction, binding, event, context, failure, node, gateId) {
|
|
271
|
+
return Effect.gen(function* () {
|
|
272
|
+
yield* writeCommandHookInput(files.inputPath, hookContext(context, event, binding, failure, node, gateId));
|
|
273
|
+
const commandResult = yield* (yield* CommandExecutor).execute(hookFunction.command, context, commandHookOptions(hookFunction, context, files));
|
|
247
274
|
if (commandResult.exitCode !== 0) return { failure: runtimeHookFailure(binding, `hook '${binding.id}' failed`, commandResult.evidence, node) };
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
return yield* readCommandHookResult(files.resultPath, binding, hookFunction, context, node);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function writeCommandHookInput(inputPath, context) {
|
|
279
|
+
return Effect.try(() => writeFileSync(inputPath, JSON.stringify(context)));
|
|
280
|
+
}
|
|
281
|
+
function commandHookOptions(hookFunction, context, files) {
|
|
282
|
+
return {
|
|
283
|
+
env: {
|
|
284
|
+
...hookEnv(hookFunction, context),
|
|
285
|
+
PIPELINE_HOOK_INPUT: files.inputPath,
|
|
286
|
+
PIPELINE_HOOK_RESULT: files.resultPath
|
|
287
|
+
},
|
|
288
|
+
extendEnv: false,
|
|
289
|
+
outputLimitBytes: hookFunction.output_limit_bytes ?? context.hookPolicy.outputLimitBytes,
|
|
290
|
+
timeout: hookFunction.timeout_ms ?? context.hookPolicy.timeoutMs
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function readCommandHookResult(resultPath, binding, hookFunction, context, node) {
|
|
294
|
+
return Effect.gen(function* () {
|
|
295
|
+
if (!(yield* Effect.sync(() => existsSync(resultPath)))) return { failure: commandHookFailure(binding, "command hook did not write PIPELINE_HOOK_RESULT", node) };
|
|
296
|
+
return yield* parseCommandHookResult(resultPath, binding, hookFunction, context, node);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function parseCommandHookResult(resultPath, binding, hookFunction, context, node) {
|
|
300
|
+
return Effect.try(() => parseAndValidateHookResult(parseJson(readFileSync(resultPath, "utf8"), "hook result"), binding, hookFunction, context, node));
|
|
256
301
|
}
|
|
257
302
|
function hookContext(context, event, binding, failure, node, gateId) {
|
|
258
303
|
const taskContext = node ? effectiveTaskContext(node, context) : context.taskContext;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OpencodeSdkService, OpencodeSdkServiceLive } from "./services/opencode-sdk-service.js";
|
|
2
|
+
import { Data, Effect } from "effect";
|
|
3
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
3
4
|
//#region src/runtime/opencode-server.ts
|
|
4
5
|
const DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
5
6
|
var OpencodeServerStartupError = class extends Data.TaggedError("OpencodeServerStartupError") {
|
|
@@ -10,30 +11,35 @@ var OpencodeServerStartupError = class extends Data.TaggedError("OpencodeServerS
|
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
+
function openOpencodeServer(options) {
|
|
15
|
+
return Effect.runPromise(Effect.provide(openOpencodeServerEffect(options), OpencodeSdkServiceLive));
|
|
16
|
+
}
|
|
17
|
+
function openOpencodeServerEffect(options) {
|
|
14
18
|
const externalUrl = options.serverUrl ?? process.env.OPENCODE_SERVER_URL ?? "";
|
|
15
|
-
if (externalUrl.length > 0) return connectExternalServer(externalUrl, options.directory);
|
|
16
|
-
return
|
|
19
|
+
if (externalUrl.length > 0) return connectExternalServer(externalUrl, options.directory).pipe(Effect.mapError(startupError));
|
|
20
|
+
return spawnOwnedServer(options);
|
|
17
21
|
}
|
|
18
22
|
function connectExternalServer(url, directory) {
|
|
19
|
-
return {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
return Effect.gen(function* () {
|
|
24
|
+
return {
|
|
25
|
+
close: () => Promise.resolve(),
|
|
26
|
+
client: yield* (yield* OpencodeSdkService).createClient({
|
|
27
|
+
baseUrl: url,
|
|
28
|
+
directory
|
|
29
|
+
}),
|
|
30
|
+
owned: false,
|
|
31
|
+
url
|
|
32
|
+
};
|
|
33
|
+
});
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const { client, server } = await spawn(spawnArgs(options));
|
|
35
|
+
function spawnOwnedServer(options) {
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const { client, server } = yield* (yield* OpencodeSdkService).spawnServer(spawnArgs(options), options.spawn);
|
|
33
38
|
return ownedHandle(client, server, options.directory);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
}).pipe(Effect.catchAll((error) => Effect.fail(startupError(error))));
|
|
40
|
+
}
|
|
41
|
+
function startupError(error) {
|
|
42
|
+
return new OpencodeServerStartupError(`Failed to start opencode server: ${errorText(error)}`, { cause: error });
|
|
37
43
|
}
|
|
38
44
|
function spawnArgs(options) {
|
|
39
45
|
return {
|