@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.
- 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 +5 -5
- 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 +7 -7
- 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-schema.d.ts +6 -6
- package/dist/runner-event-sink.js +37 -69
- package/dist/runtime/agent-node/agent-node.js +214 -173
- 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 +45 -27
- 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
package/dist/argo-submit.d.ts
CHANGED
|
@@ -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?:
|
|
41
|
+
kubeConfig?: KubernetesArgoIoDependencies["kubeConfig"];
|
|
44
42
|
workflowApi?: WorkflowApi;
|
|
45
43
|
}
|
|
46
44
|
declare function submitRunnerArgoWorkflow(rawOptions: SubmitRunnerArgoWorkflowOptions, dependencies?: SubmitRunnerArgoWorkflowDependencies): Promise<SubmitRunnerArgoWorkflowResult>;
|
package/dist/argo-submit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 };
|
package/dist/cluster-doctor.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
152
|
-
return
|
|
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
|
-
|
|
170
|
-
|
|
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 =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
200
|
+
passed
|
|
191
201
|
};
|
|
192
202
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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;
|
package/dist/config/defaults.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
115
|
+
return runConfigIoSync(parseConfigYamlAs(source, sourcePath, openCodeEcosystemManifestSchema));
|
|
115
116
|
}
|
|
116
117
|
function loadDefaultOpenCodeEcosystemManifest() {
|
|
117
|
-
return
|
|
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 };
|
package/dist/config/load.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 };
|