@oisincoveney/pipeline 2.8.0 → 2.8.1

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.
Files changed (37) hide show
  1. package/dist/argo-submit.d.ts +2 -4
  2. package/dist/argo-submit.js +80 -80
  3. package/dist/cluster-doctor.js +89 -101
  4. package/dist/config/defaults.js +9 -19
  5. package/dist/config/load.js +32 -39
  6. package/dist/config/schemas.d.ts +5 -5
  7. package/dist/mcp/gateway-error.js +15 -0
  8. package/dist/mcp/gateway.js +119 -220
  9. package/dist/moka-global-config.js +20 -20
  10. package/dist/moka-submit.d.ts +7 -7
  11. package/dist/pipeline-runtime.js +580 -371
  12. package/dist/run-state/git-refs.js +124 -94
  13. package/dist/runner-command-contract.d.ts +2 -2
  14. package/dist/runner-event-schema.d.ts +6 -6
  15. package/dist/runner-event-sink.js +37 -69
  16. package/dist/runtime/agent-node/agent-node.js +214 -173
  17. package/dist/runtime/changed-files/changed-files.js +15 -27
  18. package/dist/runtime/changed-files/index.js +2 -0
  19. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  20. package/dist/runtime/gates/gates.js +45 -27
  21. package/dist/runtime/hooks/hooks.js +74 -29
  22. package/dist/runtime/opencode-server.js +27 -21
  23. package/dist/runtime/opencode-session-executor.js +101 -44
  24. package/dist/runtime/parallel-node/parallel-node.js +24 -5
  25. package/dist/runtime/select-candidate/select-candidate.js +45 -29
  26. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  27. package/dist/runtime/services/command-executor-service.js +8 -0
  28. package/dist/runtime/services/config-io-service.js +42 -0
  29. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  30. package/dist/runtime/services/git-porcelain-service.js +38 -0
  31. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  32. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  33. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  34. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  35. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  36. package/dist/runtime/services/select-candidate-service.js +13 -0
  37. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { PipelineConfig } from "./config/schemas.js";
2
+ import { CoreApi, KubernetesArgoIoDependencies, WorkflowApi } from "./runtime/services/kubernetes-argo-service.js";
2
3
  import { workflowSubmitResultSchema } from "./workflow-submit-contract.js";
3
4
  import { z } from "zod";
4
- import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
5
5
 
6
6
  //#region src/argo-submit.d.ts
7
7
  declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
@@ -36,11 +36,9 @@ type SubmitRunnerArgoWorkflowOptions = z.input<typeof submitRunnerArgoWorkflowOp
36
36
  };
37
37
  type SubmitRunnerArgoWorkflowResult = z.infer<typeof workflowSubmitResultSchema>;
38
38
  type CommandScheduleOptions = z.input<typeof commandScheduleOptionsSchema>;
39
- type CoreApi = Pick<CoreV1Api, "createNamespacedConfigMap">;
40
- type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject">;
41
39
  interface SubmitRunnerArgoWorkflowDependencies {
42
40
  coreApi?: CoreApi;
43
- kubeConfig?: KubeConfig;
41
+ kubeConfig?: KubernetesArgoIoDependencies["kubeConfig"];
44
42
  workflowApi?: WorkflowApi;
45
43
  }
46
44
  declare function submitRunnerArgoWorkflow(rawOptions: SubmitRunnerArgoWorkflowOptions, dependencies?: SubmitRunnerArgoWorkflowDependencies): Promise<SubmitRunnerArgoWorkflowResult>;
@@ -4,11 +4,12 @@ import { buildRunnerArgoWorkflowManifest, runnerArgoWorkflowManifestSchema } fro
4
4
  import { normalizeRunnerRepositoryForSubmit } from "./git-remote-url.js";
5
5
  import { compileScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
6
6
  import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
7
+ import { KubernetesArgoService, KubernetesArgoServiceLive } from "./runtime/services/kubernetes-argo-service.js";
7
8
  import { workflowSubmitResultSchema } from "./workflow-submit-contract.js";
8
9
  import { stringify } from "yaml";
9
10
  import { z } from "zod";
11
+ import { Effect } from "effect";
10
12
  import { randomBytes } from "node:crypto";
11
- import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
12
13
  //#region src/argo-submit.ts
13
14
  const scheduleIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
14
15
  const configMapSchema = z.object({
@@ -48,7 +49,10 @@ const commandScheduleOptionsSchema = z.object({
48
49
  scheduleId: scheduleIdSchema.optional(),
49
50
  task: z.string().min(1)
50
51
  }).strict();
51
- async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
52
+ function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
53
+ return Effect.runPromise(Effect.provide(Effect.suspend(() => submitRunnerArgoWorkflowEffect(rawOptions, dependencies)), KubernetesArgoServiceLive));
54
+ }
55
+ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
52
56
  const { config, ...schemaOptions } = rawOptions;
53
57
  const options = submitRunnerArgoWorkflowOptionsSchema.parse(schemaOptions);
54
58
  const { payload, payloadJson } = normalizeRunnerPayloadForSubmit({
@@ -60,7 +64,7 @@ async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
60
64
  const scheduleArtifactConfigMapName = `pipeline-schedule-${randomBytes(6).toString("hex")}`;
61
65
  const taskDescriptorConfigMapName = `pipeline-task-descriptors-${randomBytes(6).toString("hex")}`;
62
66
  if (payload.workflow.id !== compiled.workflowId) throw new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`);
63
- const graph = compileSubmitArgoGraph(compiled);
67
+ const graphEffect = compileSubmitArgoGraph(compiled).pipe(Effect.mapError((error) => /* @__PURE__ */ new Error(`Schedule '${compiled.workflowId}' cannot be submitted: ${error.message}`)));
64
68
  const labels = {
65
69
  "pipeline.oisin.dev/project": payload.run.project,
66
70
  "pipeline.oisin.dev/run-id": payload.run.id,
@@ -91,64 +95,72 @@ async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
91
95
  serviceAccountName: options.serviceAccountName,
92
96
  taskDescriptorConfigMapName
93
97
  });
94
- const { coreApi, workflowApi } = apiClients(options, dependencies);
95
- await coreApi.createNamespacedConfigMap({
96
- body: configMapSchema.parse({
97
- apiVersion: "v1",
98
- data: { "payload.json": payloadJson },
99
- kind: "ConfigMap",
100
- metadata: {
101
- labels,
102
- name: payloadConfigMapName,
103
- namespace: options.namespace
104
- }
105
- }),
106
- namespace: options.namespace
107
- });
108
- await coreApi.createNamespacedConfigMap({
109
- body: configMapSchema.parse({
110
- apiVersion: "v1",
111
- data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
112
- kind: "ConfigMap",
113
- metadata: {
114
- labels,
115
- name: taskDescriptorConfigMapName,
116
- namespace: options.namespace
117
- }
118
- }),
119
- namespace: options.namespace
120
- });
121
- await coreApi.createNamespacedConfigMap({
122
- body: configMapSchema.parse({
123
- apiVersion: "v1",
124
- data: { "schedule.yaml": options.scheduleYaml },
125
- kind: "ConfigMap",
126
- metadata: {
127
- labels,
128
- name: scheduleArtifactConfigMapName,
129
- namespace: options.namespace
130
- }
131
- }),
132
- namespace: options.namespace
133
- });
134
- const response = await workflowApi.createNamespacedCustomObject({
135
- body: runnerArgoWorkflowManifestSchema.parse(workflow),
136
- group: "argoproj.io",
137
- namespace: options.namespace,
138
- plural: "workflows",
139
- version: "v1alpha1"
140
- });
141
- const created = z.object({ metadata: z.object({
142
- name: z.string().min(1).optional(),
143
- uid: z.string().min(1).optional()
144
- }).passthrough() }).passthrough().parse(response);
145
- return workflowSubmitResultSchema.parse({
146
- namespace: options.namespace,
147
- payloadConfigMapName,
148
- scheduleConfigMapName: scheduleArtifactConfigMapName,
149
- taskDescriptorConfigMapName,
150
- workflowName: created.metadata.name ?? workflow.metadata.name,
151
- workflowUid: created.metadata.uid
98
+ return Effect.gen(function* () {
99
+ const service = yield* KubernetesArgoService;
100
+ const graph = yield* graphEffect;
101
+ yield* service.createConfigMap({
102
+ body: configMapSchema.parse({
103
+ apiVersion: "v1",
104
+ data: { "payload.json": payloadJson },
105
+ kind: "ConfigMap",
106
+ metadata: {
107
+ labels,
108
+ name: payloadConfigMapName,
109
+ namespace: options.namespace
110
+ }
111
+ }),
112
+ dependencies,
113
+ namespace: options.namespace,
114
+ options
115
+ });
116
+ yield* service.createConfigMap({
117
+ body: configMapSchema.parse({
118
+ apiVersion: "v1",
119
+ data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
120
+ kind: "ConfigMap",
121
+ metadata: {
122
+ labels,
123
+ name: taskDescriptorConfigMapName,
124
+ namespace: options.namespace
125
+ }
126
+ }),
127
+ dependencies,
128
+ namespace: options.namespace,
129
+ options
130
+ });
131
+ yield* service.createConfigMap({
132
+ body: configMapSchema.parse({
133
+ apiVersion: "v1",
134
+ data: { "schedule.yaml": options.scheduleYaml },
135
+ kind: "ConfigMap",
136
+ metadata: {
137
+ labels,
138
+ name: scheduleArtifactConfigMapName,
139
+ namespace: options.namespace
140
+ }
141
+ }),
142
+ dependencies,
143
+ namespace: options.namespace,
144
+ options
145
+ });
146
+ const response = yield* service.createWorkflow({
147
+ body: runnerArgoWorkflowManifestSchema.parse(workflow),
148
+ dependencies,
149
+ namespace: options.namespace,
150
+ options
151
+ });
152
+ const created = z.object({ metadata: z.object({
153
+ name: z.string().min(1).optional(),
154
+ uid: z.string().min(1).optional()
155
+ }).passthrough() }).passthrough().parse(response);
156
+ return workflowSubmitResultSchema.parse({
157
+ namespace: options.namespace,
158
+ payloadConfigMapName,
159
+ scheduleConfigMapName: scheduleArtifactConfigMapName,
160
+ taskDescriptorConfigMapName,
161
+ workflowName: created.metadata.name ?? workflow.metadata.name,
162
+ workflowUid: created.metadata.uid
163
+ });
152
164
  });
153
165
  }
154
166
  function buildCommandScheduleYaml(rawOptions) {
@@ -182,25 +194,13 @@ function normalizeRunnerPayloadForSubmit(input) {
182
194
  };
183
195
  }
184
196
  function compileSubmitArgoGraph(compiled) {
185
- try {
186
- return compileArgoExecutionGraph(compiled.plan);
187
- } catch (err) {
188
- if (err instanceof ArgoGraphCompilerError) throw new Error(`Schedule '${compiled.workflowId}' cannot be submitted: ${err.message}`);
189
- throw err;
190
- }
191
- }
192
- function apiClients(options, dependencies) {
193
- if (dependencies.coreApi && dependencies.workflowApi) return {
194
- coreApi: dependencies.coreApi,
195
- workflowApi: dependencies.workflowApi
196
- };
197
- const kubeConfig = dependencies.kubeConfig ?? new KubeConfig();
198
- if (!dependencies.kubeConfig) if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
199
- else kubeConfig.loadFromDefault();
200
- return {
201
- coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
202
- workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
203
- };
197
+ return Effect.try({
198
+ try: () => compileArgoExecutionGraph(compiled.plan),
199
+ catch: (error) => {
200
+ if (error instanceof ArgoGraphCompilerError) return error;
201
+ throw error;
202
+ }
203
+ });
204
204
  }
205
205
  //#endregion
206
206
  export { buildCommandScheduleYaml, submitRunnerArgoWorkflow };
@@ -1,5 +1,6 @@
1
+ import { KubernetesArgoService, KubernetesArgoServiceLive } from "./runtime/services/kubernetes-argo-service.js";
1
2
  import { loadMokaGlobalConfig } from "./moka-global-config.js";
2
- import { execa } from "execa";
3
+ import { Effect } from "effect";
3
4
  //#region src/cluster-doctor.ts
4
5
  const DEFAULT_NAMESPACE = "momokaya-pipeline";
5
6
  const DEFAULT_RESOURCES = {
@@ -13,41 +14,46 @@ const DEFAULT_RESOURCES = {
13
14
  serviceAccountName: "pipeline-runner"
14
15
  };
15
16
  const FORBIDDEN_RE = /forbidden/i;
16
- async function runClusterDoctor(options = {}) {
17
+ function runClusterDoctor(options = {}) {
18
+ return Effect.runPromise(Effect.provide(Effect.suspend(() => runClusterDoctorEffect(options)), KubernetesArgoServiceLive));
19
+ }
20
+ function runClusterDoctorEffect(options = {}) {
17
21
  const resources = clusterResources();
18
22
  const namespace = options.namespace ?? DEFAULT_NAMESPACE;
19
23
  const kubectlOptions = {
20
24
  kubeContext: options.kubeContext,
21
25
  kubeconfigPath: options.kubeconfigPath
22
26
  };
23
- const checks = await Promise.all([
24
- checkKubectlNamespace(namespace, kubectlOptions),
25
- ...secretChecks(namespace, kubectlOptions, resources),
26
- checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, kubectlOptions),
27
- checkClusterSecretStore("openbao", kubectlOptions),
28
- checkServiceAccount(namespace, resources.serviceAccountName, kubectlOptions),
29
- checkWorkflowSubmitPermission(namespace, {
30
- resource: "workflows.argoproj.io",
31
- verb: "create",
32
- ...kubectlOptions
33
- }),
34
- checkClusterResource("argo-workflow-crd", [
35
- "get",
36
- "crd",
37
- "workflows.argoproj.io"
38
- ], kubectlOptions),
39
- checkClusterResource("argo-workflow-controller", [
40
- "get",
41
- "pods",
42
- "-A",
43
- "-l",
44
- "app=workflow-controller"
45
- ], kubectlOptions)
46
- ]);
47
- return {
48
- checks,
49
- passed: checks.every((check) => check.passed)
50
- };
27
+ return Effect.gen(function* () {
28
+ const checks = yield* Effect.all([
29
+ checkKubectlNamespace(namespace, kubectlOptions),
30
+ ...secretChecks(namespace, kubectlOptions, resources),
31
+ checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, kubectlOptions),
32
+ checkClusterSecretStore("openbao", kubectlOptions),
33
+ checkServiceAccount(namespace, resources.serviceAccountName, kubectlOptions),
34
+ checkWorkflowSubmitPermission(namespace, {
35
+ resource: "workflows.argoproj.io",
36
+ verb: "create",
37
+ ...kubectlOptions
38
+ }),
39
+ checkClusterResource("argo-workflow-crd", [
40
+ "get",
41
+ "crd",
42
+ "workflows.argoproj.io"
43
+ ], kubectlOptions),
44
+ checkClusterResource("argo-workflow-controller", [
45
+ "get",
46
+ "pods",
47
+ "-A",
48
+ "-l",
49
+ "app=workflow-controller"
50
+ ], kubectlOptions)
51
+ ], { concurrency: "unbounded" });
52
+ return {
53
+ checks,
54
+ passed: checks.every((check) => check.passed)
55
+ };
56
+ });
51
57
  }
52
58
  function defaultClusterDoctorNamespace() {
53
59
  return DEFAULT_NAMESPACE;
@@ -89,9 +95,8 @@ function checkKubectlNamespace(namespace, kubectlOptions) {
89
95
  namespace
90
96
  ], `Namespace ${namespace} missing or inaccessible.`, kubectlOptions);
91
97
  }
92
- async function checkNamespacedResource(name, args, missingDetail, kubectlOptions) {
93
- const result = await kubectl(args, kubectlOptions);
94
- return result.ok ? {
98
+ function checkNamespacedResource(name, args, missingDetail, kubectlOptions) {
99
+ return kubectl(args, kubectlOptions).pipe(Effect.map((result) => result.ok ? {
95
100
  detail: "present",
96
101
  name,
97
102
  passed: true
@@ -99,10 +104,10 @@ async function checkNamespacedResource(name, args, missingDetail, kubectlOptions
99
104
  detail: inaccessibleOrMissingDetail(name, missingDetail, result),
100
105
  name,
101
106
  passed: false
102
- };
107
+ }));
103
108
  }
104
- async function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
105
- const result = await kubectl([
109
+ function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
110
+ return kubectl([
106
111
  "get",
107
112
  "externalsecret",
108
113
  name,
@@ -110,34 +115,36 @@ async function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
110
115
  namespace,
111
116
  "-o",
112
117
  "json"
113
- ], kubectlOptions);
114
- if (!result.ok) {
115
- const missingDetail = `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`;
116
- return {
117
- detail: inaccessibleOrMissingDetail(`externalsecret/${name}`, missingDetail, result),
118
- name: `externalsecret/${name}`,
119
- passed: false
120
- };
121
- }
122
- return readyConditionCheck(`externalsecret/${name}`, result.stdout);
123
- }
124
- async function checkClusterSecretStore(name, kubectlOptions) {
125
- const result = await kubectl([
118
+ ], kubectlOptions).pipe(Effect.map((result) => {
119
+ if (!result.ok) {
120
+ const missingDetail = `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`;
121
+ return {
122
+ detail: inaccessibleOrMissingDetail(`externalsecret/${name}`, missingDetail, result),
123
+ name: `externalsecret/${name}`,
124
+ passed: false
125
+ };
126
+ }
127
+ return readyConditionCheck(`externalsecret/${name}`, result.stdout);
128
+ }));
129
+ }
130
+ function checkClusterSecretStore(name, kubectlOptions) {
131
+ return kubectl([
126
132
  "get",
127
133
  "clustersecretstore",
128
134
  name,
129
135
  "-o",
130
136
  "json"
131
- ], kubectlOptions);
132
- if (!result.ok) {
133
- const missingDetail = `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`;
134
- return {
135
- detail: inaccessibleOrMissingDetail(`clustersecretstore/${name}`, missingDetail, result),
136
- name: `clustersecretstore/${name}`,
137
- passed: false
138
- };
139
- }
140
- return readyConditionCheck(`clustersecretstore/${name}`, result.stdout);
137
+ ], kubectlOptions).pipe(Effect.map((result) => {
138
+ if (!result.ok) {
139
+ const missingDetail = `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`;
140
+ return {
141
+ detail: inaccessibleOrMissingDetail(`clustersecretstore/${name}`, missingDetail, result),
142
+ name: `clustersecretstore/${name}`,
143
+ passed: false
144
+ };
145
+ }
146
+ return readyConditionCheck(`clustersecretstore/${name}`, result.stdout);
147
+ }));
141
148
  }
142
149
  function checkServiceAccount(namespace, name, kubectlOptions) {
143
150
  return checkNamespacedResource(`serviceaccount/${name}`, [
@@ -148,15 +155,15 @@ function checkServiceAccount(namespace, name, kubectlOptions) {
148
155
  namespace
149
156
  ], `ServiceAccount ${name} missing in ${namespace}; runner pods must use this account for workflow execution.`, kubectlOptions);
150
157
  }
151
- async function checkWorkflowSubmitPermission(namespace, options) {
152
- return (await kubectl([
158
+ function checkWorkflowSubmitPermission(namespace, options) {
159
+ return kubectl([
153
160
  "auth",
154
161
  "can-i",
155
162
  options.verb,
156
163
  options.resource,
157
164
  "-n",
158
165
  namespace
159
- ], options)).stdout.trim() === "yes" ? {
166
+ ], options).pipe(Effect.map((result) => result.stdout.trim() === "yes" ? {
160
167
  detail: `current kube identity can ${options.verb} ${options.resource}`,
161
168
  name: "rbac/workflow-create",
162
169
  passed: true
@@ -164,11 +171,10 @@ async function checkWorkflowSubmitPermission(namespace, options) {
164
171
  detail: `current kube identity cannot ${options.verb} ${options.resource}; check submitter RBAC for Workflow creation.`,
165
172
  name: "rbac/workflow-create",
166
173
  passed: false
167
- };
174
+ }));
168
175
  }
169
- async function checkClusterResource(name, args, kubectlOptions) {
170
- const result = await kubectl(args, kubectlOptions);
171
- return result.ok ? {
176
+ function checkClusterResource(name, args, kubectlOptions) {
177
+ return kubectl(args, kubectlOptions).pipe(Effect.map((result) => result.ok ? {
172
178
  detail: "present",
173
179
  name,
174
180
  passed: true
@@ -176,46 +182,28 @@ async function checkClusterResource(name, args, kubectlOptions) {
176
182
  detail: isForbidden(result) ? inaccessibleDetail(name, result) : result.stderr || "missing or inaccessible",
177
183
  name,
178
184
  passed: false
179
- };
185
+ }));
186
+ }
187
+ function findReadyCondition(source) {
188
+ return parseJson(source).status?.conditions?.find((condition) => condition.type === "Ready");
189
+ }
190
+ function readyDetail(ready, passed) {
191
+ const fallback = passed ? "Ready=True" : "Ready condition is missing or not True";
192
+ return ready?.message || fallback;
180
193
  }
181
194
  function readyConditionCheck(name, source) {
182
- const ready = parseJson(source).status?.conditions?.find((condition) => condition.type === "Ready");
183
- return ready?.status === "True" ? {
184
- detail: ready.message || "Ready=True",
185
- name,
186
- passed: true
187
- } : {
188
- detail: ready?.message || "Ready condition is missing or not True",
195
+ const ready = findReadyCondition(source);
196
+ const passed = ready?.status === "True";
197
+ return {
198
+ detail: readyDetail(ready, passed),
189
199
  name,
190
- passed: false
200
+ passed
191
201
  };
192
202
  }
193
- async function kubectl(args, options) {
194
- try {
195
- const result = await execa("kubectl", kubectlArgs(args, options.kubeContext), {
196
- env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
197
- stdin: "ignore"
198
- });
199
- return {
200
- ok: true,
201
- stderr: result.stderr,
202
- stdout: result.stdout
203
- };
204
- } catch (err) {
205
- const error = err;
206
- return {
207
- ok: false,
208
- stderr: (error.stderr || error.shortMessage || "kubectl failed").trim(),
209
- stdout: (error.stdout || "").trim()
210
- };
211
- }
212
- }
213
- function kubectlArgs(args, kubeContext) {
214
- return kubeContext ? [
215
- "--context",
216
- kubeContext,
217
- ...args
218
- ] : args;
203
+ function kubectl(args, options) {
204
+ return Effect.gen(function* () {
205
+ return yield* (yield* KubernetesArgoService).kubectl(args, options);
206
+ });
219
207
  }
220
208
  function inaccessibleOrMissingDetail(name, missingDetail, result) {
221
209
  return isForbidden(result) ? inaccessibleDetail(name, result) : missingDetail;
@@ -1,7 +1,6 @@
1
- import { PipelineConfigError, configIssuesFromZodError, validationError } from "./schemas.js";
2
- import { parseDocument } from "yaml";
1
+ import { ConfigIoService, parseConfigYamlAs, runConfigIoSync } from "../runtime/services/config-io-service.js";
3
2
  import { z } from "zod";
4
- import { readFileSync } from "node:fs";
3
+ import { Effect } from "effect";
5
4
  //#region src/config/defaults.ts
6
5
  const PIPELINE_CONFIG_PATH = ".pipeline/pipeline.yaml";
7
6
  const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
@@ -9,7 +8,9 @@ const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
9
8
  const OPENCODE_ECOSYSTEM_MANIFEST_PATH = "defaults/opencode-ecosystem.yaml";
10
9
  const DEFAULT_PACKAGE_DEFAULTS_ROOT = new URL("../../defaults/", import.meta.url);
11
10
  function loadDefaultYaml(filename) {
12
- return readFileSync(new URL(filename, DEFAULT_PACKAGE_DEFAULTS_ROOT), "utf8");
11
+ return runConfigIoSync(Effect.gen(function* () {
12
+ return yield* (yield* ConfigIoService).readText(new URL(filename, DEFAULT_PACKAGE_DEFAULTS_ROOT));
13
+ }));
13
14
  }
14
15
  const PACKAGE_DEFAULT_RUNNERS_YAML = loadDefaultYaml("runners.yaml");
15
16
  const PACKAGE_DEFAULT_PROFILES_YAML = loadDefaultYaml("profiles.yaml");
@@ -111,24 +112,13 @@ const openCodeEcosystemManifestSchema = z.object({
111
112
  version: z.literal(1)
112
113
  }).strict();
113
114
  function parseOpenCodeEcosystemManifest(source, sourcePath = OPENCODE_ECOSYSTEM_MANIFEST_PATH) {
114
- return parseYamlAs(source, sourcePath, openCodeEcosystemManifestSchema);
115
+ return runConfigIoSync(parseConfigYamlAs(source, sourcePath, openCodeEcosystemManifestSchema));
115
116
  }
116
117
  function loadDefaultOpenCodeEcosystemManifest() {
117
- return parseOpenCodeEcosystemManifest(readFileSync(DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST_URL, "utf8"));
118
+ return runConfigIoSync(Effect.gen(function* () {
119
+ return yield* parseConfigYamlAs(yield* (yield* ConfigIoService).readText(DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST_URL), OPENCODE_ECOSYSTEM_MANIFEST_PATH, openCodeEcosystemManifestSchema);
120
+ }));
118
121
  }
119
122
  const DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST = loadDefaultOpenCodeEcosystemManifest();
120
- function parseYamlAs(source, sourcePath, schema) {
121
- const document = parseDocument(source, {
122
- prettyErrors: false,
123
- uniqueKeys: true
124
- });
125
- if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
126
- message: err.message,
127
- path: sourcePath
128
- })));
129
- const parsed = schema.safeParse(document.toJS());
130
- if (!parsed.success) throw validationError(configIssuesFromZodError(parsed.error));
131
- return parsed.data;
132
- }
133
123
  //#endregion
134
124
  export { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST, OPENCODE_ECOSYSTEM_MANIFEST_PATH, PACKAGE_DEFAULT_PIPELINE_YAML, PACKAGE_DEFAULT_PROFILES_YAML, PACKAGE_DEFAULT_RUNNERS_YAML, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, RUNNERS_CONFIG_PATH, parseOpenCodeEcosystemManifest };
@@ -1,13 +1,14 @@
1
- import { PipelineConfigError, configIssuesFromZodError, pipelineFileSchema, profilesFileSchema, runnersFileSchema, validationError } from "./schemas.js";
1
+ import { pipelineFileSchema, profilesFileSchema, runnersFileSchema } from "./schemas.js";
2
+ import { parseConfigYamlAs, runConfigIoSync } from "../runtime/services/config-io-service.js";
2
3
  import { PACKAGE_DEFAULT_PIPELINE_YAML, PACKAGE_DEFAULT_PROFILES_YAML, PACKAGE_DEFAULT_RUNNERS_YAML, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, RUNNERS_CONFIG_PATH } from "./defaults.js";
3
4
  import { validatePipelineConfig } from "./validate.js";
4
- import { parseDocument } from "yaml";
5
+ import { Effect } from "effect";
5
6
  //#region src/config/load.ts
6
7
  function loadPipelineConfig(projectRoot, options = {}) {
7
8
  return loadPackagePipelineConfig(projectRoot, options);
8
9
  }
9
10
  function loadPackagePipelineConfig(projectRoot, options = {}) {
10
- return parsePipelineConfigParts({
11
+ return runConfigIoSync(parsePipelineConfigPartsEffect({
11
12
  pipeline: PACKAGE_DEFAULT_PIPELINE_YAML,
12
13
  profiles: PACKAGE_DEFAULT_PROFILES_YAML,
13
14
  runners: PACKAGE_DEFAULT_RUNNERS_YAML
@@ -15,7 +16,7 @@ function loadPackagePipelineConfig(projectRoot, options = {}) {
15
16
  pipeline: "@oisincoveney/pipeline/defaults/pipeline.yaml",
16
17
  profiles: "@oisincoveney/pipeline/defaults/profiles.yaml",
17
18
  runners: "@oisincoveney/pipeline/defaults/runners.yaml"
18
- }, options);
19
+ }, options));
19
20
  }
20
21
  function parsePipelineConfigYaml(source, sourcePath = PIPELINE_CONFIG_PATH, projectRoot) {
21
22
  return parsePipelineConfigParts({
@@ -48,43 +49,35 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
48
49
  profiles: PROFILES_CONFIG_PATH,
49
50
  runners: RUNNERS_CONFIG_PATH
50
51
  }, options = {}) {
51
- const runners = parseYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
52
- const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
53
- const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
54
- return validatePipelineConfig({
55
- default_workflow: pipeline.default_workflow,
56
- ...durabilityField(pipeline.durability),
57
- ...pipe83Fields(pipeline),
58
- entrypoints: pipeline.entrypoints,
59
- hooks: pipeline.hooks,
60
- ...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
61
- mcp_servers: profiles.mcp_servers,
62
- ...pipeline.orchestrator ? { orchestrator: pipeline.orchestrator } : {},
63
- profiles: profiles.profiles,
64
- runner_command: pipeline.runner_command,
65
- rules: profiles.rules,
66
- runners: runners.runners,
67
- scheduler: pipeline.scheduler,
68
- schedules: pipeline.schedules,
69
- skills: profiles.skills,
70
- ...pipeline.task_context ? { task_context: pipeline.task_context } : {},
71
- token_budget: pipeline.token_budget,
72
- version: 1,
73
- workflows: pipeline.workflows
74
- }, projectRoot, options);
52
+ return runConfigIoSync(parsePipelineConfigPartsEffect(sources, projectRoot, sourcePaths, options));
75
53
  }
76
- function parseYamlAs(source, sourcePath, schema) {
77
- const document = parseDocument(source, {
78
- prettyErrors: false,
79
- uniqueKeys: true
54
+ function parsePipelineConfigPartsEffect(sources, projectRoot, sourcePaths, options) {
55
+ return Effect.gen(function* () {
56
+ const runners = yield* parseConfigYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
57
+ const profiles = yield* parseConfigYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
58
+ const pipeline = yield* parseConfigYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
59
+ return validatePipelineConfig({
60
+ default_workflow: pipeline.default_workflow,
61
+ ...durabilityField(pipeline.durability),
62
+ ...pipe83Fields(pipeline),
63
+ entrypoints: pipeline.entrypoints,
64
+ hooks: pipeline.hooks,
65
+ ...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
66
+ mcp_servers: profiles.mcp_servers,
67
+ ...pipeline.orchestrator ? { orchestrator: pipeline.orchestrator } : {},
68
+ profiles: profiles.profiles,
69
+ runner_command: pipeline.runner_command,
70
+ rules: profiles.rules,
71
+ runners: runners.runners,
72
+ scheduler: pipeline.scheduler,
73
+ schedules: pipeline.schedules,
74
+ skills: profiles.skills,
75
+ ...pipeline.task_context ? { task_context: pipeline.task_context } : {},
76
+ token_budget: pipeline.token_budget,
77
+ version: 1,
78
+ workflows: pipeline.workflows
79
+ }, projectRoot, options);
80
80
  });
81
- if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
82
- message: err.message,
83
- path: sourcePath
84
- })));
85
- const parsed = schema.safeParse(document.toJS());
86
- if (!parsed.success) throw validationError(configIssuesFromZodError(parsed.error));
87
- return parsed.data;
88
81
  }
89
82
  //#endregion
90
83
  export { loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml };