@oisincoveney/pipeline 1.23.3 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -66
- package/dist/argo-graph.js +94 -0
- package/dist/argo-submit.d.ts +61 -0
- package/dist/argo-submit.js +206 -0
- package/dist/argo-workflow.d.ts +159 -0
- package/dist/argo-workflow.js +410 -0
- package/dist/commands/pipeline-command.js +6 -2
- package/dist/commands/runner-command-command.js +13 -0
- package/dist/config.d.ts +11 -20
- package/dist/config.js +19 -28
- package/dist/index.js +107 -28
- package/dist/install-commands.js +9 -9
- package/dist/mcp/bootstrap.js +1 -1
- package/dist/mcp/gateway.js +1 -1
- package/dist/moka-submit.js +191 -0
- package/dist/path-refs.js +1 -1
- package/dist/pipeline-runtime.d.ts +6 -1
- package/dist/pipeline-runtime.js +44 -101
- package/dist/run-state/git-refs.js +74 -0
- package/dist/runner-command/finalize.js +74 -0
- package/dist/runner-command/run.js +130 -0
- package/dist/runner-command/task-descriptor.js +17 -0
- package/dist/{runner-job-contract.d.ts → runner-command-contract.d.ts} +40 -88
- package/dist/{runner-job-contract.js → runner-command-contract.js} +85 -42
- package/dist/runner-event-sink.js +3 -3
- package/dist/runner.js +1 -1
- package/dist/runtime/agent-node/agent-node.js +2 -2
- package/dist/runtime/changed-files/changed-files.js +1 -1
- package/dist/runtime/context/context.js +3 -12
- package/dist/runtime/contracts/contracts.d.ts +1 -2
- package/dist/runtime/goal-state.js +1 -1
- package/dist/runtime/hooks/hooks.js +2 -2
- package/dist/runtime/json-validation/json-validation.js +1 -1
- package/dist/runtime/parallel-node/parallel-node.js +2 -7
- package/dist/runtime-machines/workflow-machine.js +1 -1
- package/dist/schedule-planner.d.ts +3 -315
- package/dist/schedule-planner.js +6 -44
- package/dist/standard-output-schemas.js +1 -1
- package/dist/workflow-planner.d.ts +0 -2
- package/dist/workflow-planner.js +1 -3
- package/docs/adr-opencode-first-goal-loop-runtime.md +2 -2
- package/docs/operator-guide.md +116 -132
- package/docs/pipeline-console-runner-contract.md +17 -17
- package/package.json +15 -10
- package/dist/commands/runner-job-command.js +0 -14
- package/dist/k8s-submit.js +0 -267
- package/dist/runner-job/delivery.js +0 -134
- package/dist/runner-job/devspace.js +0 -34
- package/dist/runner-job/pr-summary.js +0 -117
- package/dist/runner-job/run.js +0 -409
- package/dist/runner-job/workspace.js +0 -86
- package/dist/runner-job-k8s.d.ts +0 -68
- package/dist/runner-job-k8s.js +0 -146
- package/dist/runtime/worktrees/index.js +0 -2
- package/dist/runtime/worktrees/worktrees.js +0 -65
package/README.md
CHANGED
|
@@ -116,20 +116,22 @@ target repository.
|
|
|
116
116
|
## Pipeline Console Runner Image
|
|
117
117
|
|
|
118
118
|
`oisin-pipeline` is also the runner package/image used by `pipeline-console`.
|
|
119
|
-
The
|
|
120
|
-
storage, Kueue discovery, and UI rendering. This package owns the
|
|
121
|
-
`runner-
|
|
122
|
-
|
|
123
|
-
flushing.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
the event auth token
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
The control plane owns Argo Workflow submission, run listing, cancellation,
|
|
120
|
+
event storage, Kueue discovery, and UI rendering. This package owns the
|
|
121
|
+
in-container `runner-command` command used by Argo Workflow DAG tasks: payload
|
|
122
|
+
validation, direct argv execution, event translation, authenticated event
|
|
123
|
+
posting, signal cancellation, and final event flushing.
|
|
124
|
+
|
|
125
|
+
Argo starts the image with payload, schedule, and per-task descriptor files
|
|
126
|
+
mounted from ConfigMaps, plus the event auth token mounted from a Secret. The
|
|
127
|
+
runner reads `--payload-file`, `--schedule-file`, and `/etc/pipeline/task.json`;
|
|
128
|
+
the payload contains the workflow id and the task descriptor contains the node
|
|
129
|
+
id. The event auth token still comes from the file path specified in
|
|
130
|
+
`events.authTokenFile`; payload JSON, task identity, and auth token material are
|
|
131
|
+
not delivered through environment variables. The payload contract is documented in
|
|
130
132
|
[`docs/pipeline-console-runner-contract.md`](docs/pipeline-console-runner-contract.md).
|
|
131
133
|
The executable contract is exported from
|
|
132
|
-
`@oisincoveney/pipeline/runner-
|
|
134
|
+
`@oisincoveney/pipeline/runner-command-contract` for payload construction,
|
|
133
135
|
validation, contract-version checks, and JSON Schema generation.
|
|
134
136
|
Use `PIPELINE_TARGET_PATH=/path/to/worktree` when the checked-out target repo is
|
|
135
137
|
mounted somewhere other than the process working directory.
|
|
@@ -225,25 +227,18 @@ workflow. See `docs/config-architecture.md` for the host support matrix.
|
|
|
225
227
|
|
|
226
228
|
### Structural Parallelism
|
|
227
229
|
|
|
228
|
-
Workflows
|
|
229
|
-
`kind:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
auditable in YAML.
|
|
230
|
+
Workflows and generated schedules can express fixed parallel structure. A
|
|
231
|
+
`kind: parallel` node contains a fixed set of child nodes that run concurrently
|
|
232
|
+
after dependencies pass. A `kind: group` node groups existing nodes behind a
|
|
233
|
+
single dependency target. This is structural parallelism, not dynamic fanout:
|
|
234
|
+
agents may route work to tracks, but the branch topology stays auditable in YAML
|
|
235
|
+
or in the generated schedule artifact.
|
|
235
236
|
|
|
236
|
-
The
|
|
237
|
+
The scheduler emits explicit root DAGs for epics:
|
|
237
238
|
|
|
238
239
|
```yaml
|
|
239
|
-
entrypoints:
|
|
240
|
-
epic:
|
|
241
|
-
workflow: epic-drain
|
|
242
|
-
description: Route an epic's tickets into specialist tracks, run them in parallel, then thermo-nuclear review.
|
|
243
|
-
|
|
244
240
|
workflows:
|
|
245
|
-
|
|
246
|
-
description: Research, route, parallel-implement tracks in isolated worktrees, integrate, thermo-nuclear review.
|
|
241
|
+
root:
|
|
247
242
|
nodes:
|
|
248
243
|
- id: research
|
|
249
244
|
kind: agent
|
|
@@ -257,39 +252,26 @@ workflows:
|
|
|
257
252
|
needs: [plan]
|
|
258
253
|
nodes:
|
|
259
254
|
- id: test
|
|
260
|
-
kind:
|
|
261
|
-
|
|
262
|
-
worktree_root: .pipeline/runs/${runId}/test
|
|
255
|
+
kind: agent
|
|
256
|
+
profile: pipeline-code-writer
|
|
263
257
|
- id: frontend
|
|
264
|
-
kind:
|
|
265
|
-
|
|
266
|
-
worktree_root: .pipeline/runs/${runId}/frontend
|
|
258
|
+
kind: agent
|
|
259
|
+
profile: pipeline-code-writer
|
|
267
260
|
- id: backend
|
|
268
|
-
kind:
|
|
269
|
-
|
|
270
|
-
worktree_root: .pipeline/runs/${runId}/backend
|
|
261
|
+
kind: agent
|
|
262
|
+
profile: pipeline-code-writer
|
|
271
263
|
- id: k8s
|
|
272
|
-
kind:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- id: merge
|
|
276
|
-
kind: builtin
|
|
277
|
-
builtin: drain-merge
|
|
278
|
-
needs: [implement]
|
|
279
|
-
- id: review
|
|
264
|
+
kind: agent
|
|
265
|
+
profile: pipeline-code-writer
|
|
266
|
+
- id: verify
|
|
280
267
|
kind: agent
|
|
281
|
-
profile: pipeline-
|
|
282
|
-
needs: [
|
|
283
|
-
gates:
|
|
284
|
-
- { id: review-verdict, kind: verdict, target: stdout }
|
|
268
|
+
profile: pipeline-verifier
|
|
269
|
+
needs: [implement]
|
|
285
270
|
```
|
|
286
271
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
branches share a base SHA, and merges passing branches into an integration
|
|
291
|
-
branch in declaration order. It reports merge conflicts; it does not resolve
|
|
292
|
-
them automatically.
|
|
272
|
+
For Argo execution, Git refs carry node state across DAG tasks. Runner tasks
|
|
273
|
+
derive state refs from `payload.run.id`, `payload.workflow.id`, and the mounted
|
|
274
|
+
task descriptor node id.
|
|
293
275
|
|
|
294
276
|
Default profile skills and generated host resources are installed by
|
|
295
277
|
`pipe init`. Runtime MCP projection and host-specific isolation policy live in
|
|
@@ -367,12 +349,6 @@ runners:
|
|
|
367
349
|
- Parallel DAG batches run concurrently after dependencies and gates pass.
|
|
368
350
|
- `kind: parallel` child sets are fixed in YAML; routing agents decide which
|
|
369
351
|
work belongs in each declared track, not how many tracks exist.
|
|
370
|
-
- `kind: workflow` nodes invoke named workflows and can run in isolated
|
|
371
|
-
worktrees when `worktree_root` is set.
|
|
372
|
-
- Worktree roots support `${runId}` and `${nodeId}` templates and should live
|
|
373
|
-
under `.pipeline/runs/` for generated run artifacts.
|
|
374
|
-
- `drain-merge` merges passing worktree branches in declaration order and
|
|
375
|
-
reports conflicts for manual resolution.
|
|
376
352
|
- Workflow execution can cap parallelism and enable fail-fast batch stopping.
|
|
377
353
|
- Nodes can declare bounded retries, retry reasons, backoff, and execution
|
|
378
354
|
timeouts.
|
|
@@ -418,11 +394,10 @@ Runner Job producers can import the shared payload contract:
|
|
|
418
394
|
|
|
419
395
|
```ts
|
|
420
396
|
import {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
} from "@oisincoveney/pipeline/runner-job-contract";
|
|
397
|
+
buildRunnerCommandPayload,
|
|
398
|
+
parseRunnerCommandPayload,
|
|
399
|
+
runnerCommandPayloadSchema,
|
|
400
|
+
} from "@oisincoveney/pipeline/runner-command-contract";
|
|
426
401
|
```
|
|
427
402
|
|
|
428
403
|
## Verification
|
|
@@ -437,4 +412,6 @@ bun run build:cli
|
|
|
437
412
|
```
|
|
438
413
|
|
|
439
414
|
|
|
440
|
-
|
|
415
|
+
Argo Workflows can be rendered with `buildRunnerArgoWorkflowManifest` from
|
|
416
|
+
`@oisincoveney/pipeline/argo-workflow` and submitted with
|
|
417
|
+
`submitRunnerArgoWorkflow` from `@oisincoveney/pipeline/argo-submit`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/argo-graph.ts
|
|
3
|
+
const EXECUTABLE_NODE_KINDS = [
|
|
4
|
+
"agent",
|
|
5
|
+
"builtin",
|
|
6
|
+
"command"
|
|
7
|
+
];
|
|
8
|
+
const argoExecutableTaskSchema = z.object({
|
|
9
|
+
dependencies: z.array(z.string().min(1)),
|
|
10
|
+
nodeId: z.string().min(1),
|
|
11
|
+
taskName: z.string().min(1),
|
|
12
|
+
templateName: z.string().min(1)
|
|
13
|
+
}).strict();
|
|
14
|
+
const argoExecutionGraphSchema = z.object({
|
|
15
|
+
terminalNodeIds: z.array(z.string().min(1)),
|
|
16
|
+
tasks: z.array(argoExecutableTaskSchema).min(1),
|
|
17
|
+
terminalTaskNames: z.array(z.string().min(1)),
|
|
18
|
+
workflowId: z.string().min(1)
|
|
19
|
+
}).strict();
|
|
20
|
+
function compileArgoExecutionGraph(plan) {
|
|
21
|
+
const compiler = new ArgoGraphCompiler(plan);
|
|
22
|
+
return argoExecutionGraphSchema.parse(compiler.compile());
|
|
23
|
+
}
|
|
24
|
+
var ArgoGraphCompiler = class {
|
|
25
|
+
nodeById = /* @__PURE__ */ new Map();
|
|
26
|
+
plan;
|
|
27
|
+
tasks = [];
|
|
28
|
+
constructor(plan) {
|
|
29
|
+
this.plan = plan;
|
|
30
|
+
this.indexNodes(plan.topologicalOrder);
|
|
31
|
+
}
|
|
32
|
+
compile() {
|
|
33
|
+
this.compileNodes(this.plan.topologicalOrder, []);
|
|
34
|
+
return {
|
|
35
|
+
terminalNodeIds: this.terminalTasks().map((task) => task.nodeId),
|
|
36
|
+
tasks: this.tasks,
|
|
37
|
+
terminalTaskNames: this.terminalTasks().map((task) => task.taskName),
|
|
38
|
+
workflowId: this.plan.workflowId
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
indexNodes(nodes) {
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
if (this.nodeById.has(node.id)) throw new Error(`Argo schedule contains duplicate node id '${node.id}'`);
|
|
44
|
+
this.nodeById.set(node.id, node);
|
|
45
|
+
this.indexNodes(node.children ?? []);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
compileNodes(nodes, inheritedNeeds) {
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (isExecutableNode(node)) {
|
|
51
|
+
const dependencies = this.resolveDependencyTaskNames([...inheritedNeeds, ...node.needs]);
|
|
52
|
+
const task = argoExecutableTaskSchema.parse({
|
|
53
|
+
dependencies,
|
|
54
|
+
nodeId: node.id,
|
|
55
|
+
taskName: argoTaskName(node.id),
|
|
56
|
+
templateName: argoTemplateName(node.id)
|
|
57
|
+
});
|
|
58
|
+
this.tasks.push(task);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (node.kind === "group") continue;
|
|
62
|
+
if (node.kind === "parallel") this.compileNodes(node.children ?? [], [...inheritedNeeds, ...node.needs]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
resolveDependencyTaskNames(nodeIds) {
|
|
66
|
+
return unique(nodeIds.flatMap((nodeId) => this.resolveDependencyNodeIds(nodeId).map((id) => argoTaskName(id))));
|
|
67
|
+
}
|
|
68
|
+
resolveDependencyNodeIds(nodeId) {
|
|
69
|
+
const node = this.nodeById.get(nodeId);
|
|
70
|
+
if (!node) return [];
|
|
71
|
+
if (isExecutableNode(node)) return [node.id];
|
|
72
|
+
if (node.kind === "group") return unique([...node.nodes ?? [], ...node.needs].flatMap((id) => this.resolveDependencyNodeIds(id)));
|
|
73
|
+
if (node.kind === "parallel") return unique((node.children ?? []).flatMap((child) => this.resolveDependencyNodeIds(child.id)));
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
terminalTasks() {
|
|
77
|
+
const dependedOn = new Set(this.tasks.flatMap((task) => task.dependencies));
|
|
78
|
+
return this.tasks.filter((task) => !dependedOn.has(task.taskName));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function isExecutableNode(node) {
|
|
82
|
+
return EXECUTABLE_NODE_KINDS.includes(node.kind);
|
|
83
|
+
}
|
|
84
|
+
function argoTaskName(nodeId) {
|
|
85
|
+
return `node-${nodeId}`;
|
|
86
|
+
}
|
|
87
|
+
function argoTemplateName(nodeId) {
|
|
88
|
+
return `task-${nodeId}`;
|
|
89
|
+
}
|
|
90
|
+
function unique(values) {
|
|
91
|
+
return [...new Set(values)];
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { compileArgoExecutionGraph };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PipelineConfig } from "./config.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
4
|
+
|
|
5
|
+
//#region src/argo-submit.d.ts
|
|
6
|
+
declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
|
|
7
|
+
codexAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
8
|
+
eventAuthSecretKey: z.ZodOptional<z.ZodString>;
|
|
9
|
+
eventAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
10
|
+
generateName: z.ZodOptional<z.ZodString>;
|
|
11
|
+
githubAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
12
|
+
image: z.ZodOptional<z.ZodString>;
|
|
13
|
+
imagePullPolicy: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
Always: "Always";
|
|
15
|
+
IfNotPresent: "IfNotPresent";
|
|
16
|
+
Never: "Never";
|
|
17
|
+
}>>;
|
|
18
|
+
imagePullSecretName: z.ZodOptional<z.ZodString>;
|
|
19
|
+
kubeconfigPath: z.ZodOptional<z.ZodString>;
|
|
20
|
+
name: z.ZodOptional<z.ZodString>;
|
|
21
|
+
namespace: z.ZodDefault<z.ZodString>;
|
|
22
|
+
opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
23
|
+
orchestrator: z.ZodDefault<z.ZodEnum<{
|
|
24
|
+
codex: "codex";
|
|
25
|
+
opencode: "opencode";
|
|
26
|
+
}>>;
|
|
27
|
+
payloadJson: z.ZodString;
|
|
28
|
+
queueName: z.ZodOptional<z.ZodString>;
|
|
29
|
+
scheduleYaml: z.ZodString;
|
|
30
|
+
serviceAccountName: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strict>;
|
|
32
|
+
declare const submitRunnerArgoWorkflowResultSchema: z.ZodObject<{
|
|
33
|
+
namespace: z.ZodString;
|
|
34
|
+
payloadConfigMapName: z.ZodString;
|
|
35
|
+
scheduleConfigMapName: z.ZodString;
|
|
36
|
+
taskDescriptorConfigMapName: z.ZodString;
|
|
37
|
+
workflowName: z.ZodString;
|
|
38
|
+
workflowUid: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
declare const commandScheduleOptionsSchema: z.ZodObject<{
|
|
41
|
+
command: z.ZodArray<z.ZodString>;
|
|
42
|
+
generatedAt: z.ZodDefault<z.ZodDate>;
|
|
43
|
+
scheduleId: z.ZodOptional<z.ZodString>;
|
|
44
|
+
task: z.ZodString;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
type SubmitRunnerArgoWorkflowOptions = z.input<typeof submitRunnerArgoWorkflowOptionsSchema> & {
|
|
47
|
+
config: PipelineConfig;
|
|
48
|
+
};
|
|
49
|
+
type SubmitRunnerArgoWorkflowResult = z.infer<typeof submitRunnerArgoWorkflowResultSchema>;
|
|
50
|
+
type CommandScheduleOptions = z.input<typeof commandScheduleOptionsSchema>;
|
|
51
|
+
type CoreApi = Pick<CoreV1Api, "createNamespacedConfigMap">;
|
|
52
|
+
type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject" | "getClusterCustomObject">;
|
|
53
|
+
interface SubmitRunnerArgoWorkflowDependencies {
|
|
54
|
+
coreApi?: CoreApi;
|
|
55
|
+
kubeConfig?: KubeConfig;
|
|
56
|
+
workflowApi?: WorkflowApi;
|
|
57
|
+
}
|
|
58
|
+
declare function submitRunnerArgoWorkflow(rawOptions: SubmitRunnerArgoWorkflowOptions, dependencies?: SubmitRunnerArgoWorkflowDependencies): Promise<SubmitRunnerArgoWorkflowResult>;
|
|
59
|
+
declare function buildCommandScheduleYaml(rawOptions: CommandScheduleOptions): string;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { CommandScheduleOptions, SubmitRunnerArgoWorkflowDependencies, SubmitRunnerArgoWorkflowOptions, SubmitRunnerArgoWorkflowResult, buildCommandScheduleYaml, submitRunnerArgoWorkflow };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { compileArgoExecutionGraph } from "./argo-graph.js";
|
|
2
|
+
import { buildRunnerTaskDescriptor } from "./runner-command/task-descriptor.js";
|
|
3
|
+
import { buildRunnerArgoWorkflowManifest, runnerArgoWorkflowManifestSchema } from "./argo-workflow.js";
|
|
4
|
+
import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
|
|
5
|
+
import { compileScheduleArtifact, parseScheduleArtifact } from "./schedule-planner.js";
|
|
6
|
+
import { stringify } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
10
|
+
//#region src/argo-submit.ts
|
|
11
|
+
const scheduleIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
12
|
+
const configMapSchema = z.object({
|
|
13
|
+
apiVersion: z.literal("v1"),
|
|
14
|
+
data: z.record(z.string().min(1), z.string()),
|
|
15
|
+
kind: z.literal("ConfigMap"),
|
|
16
|
+
metadata: z.object({
|
|
17
|
+
labels: z.record(z.string().min(1), z.string().min(1)).optional(),
|
|
18
|
+
name: z.string().min(1),
|
|
19
|
+
namespace: z.string().min(1)
|
|
20
|
+
}).strict()
|
|
21
|
+
}).strict();
|
|
22
|
+
const submitRunnerArgoWorkflowOptionsSchema = z.object({
|
|
23
|
+
codexAuthSecretName: z.string().min(1).optional(),
|
|
24
|
+
eventAuthSecretKey: z.string().min(1).optional(),
|
|
25
|
+
eventAuthSecretName: z.string().min(1).optional(),
|
|
26
|
+
generateName: z.string().min(1).optional(),
|
|
27
|
+
githubAuthSecretName: z.string().min(1).optional(),
|
|
28
|
+
image: z.string().min(1).optional(),
|
|
29
|
+
imagePullPolicy: z.enum([
|
|
30
|
+
"Always",
|
|
31
|
+
"IfNotPresent",
|
|
32
|
+
"Never"
|
|
33
|
+
]).optional(),
|
|
34
|
+
imagePullSecretName: z.string().min(1).optional(),
|
|
35
|
+
kubeconfigPath: z.string().min(1).optional(),
|
|
36
|
+
name: z.string().min(1).optional(),
|
|
37
|
+
namespace: z.string().min(1).default("momokaya-pipeline"),
|
|
38
|
+
opencodeAuthSecretName: z.string().min(1).optional(),
|
|
39
|
+
orchestrator: z.enum(["codex", "opencode"]).default("opencode"),
|
|
40
|
+
payloadJson: z.string().min(1),
|
|
41
|
+
queueName: z.string().min(1).optional(),
|
|
42
|
+
scheduleYaml: z.string().min(1),
|
|
43
|
+
serviceAccountName: z.string().min(1).optional()
|
|
44
|
+
}).strict().refine((options) => options.name !== void 0 || options.generateName !== void 0, { message: "Argo submit options must declare name or generateName" });
|
|
45
|
+
const submitRunnerArgoWorkflowResultSchema = z.object({
|
|
46
|
+
namespace: z.string().min(1),
|
|
47
|
+
payloadConfigMapName: z.string().min(1),
|
|
48
|
+
scheduleConfigMapName: z.string().min(1),
|
|
49
|
+
taskDescriptorConfigMapName: z.string().min(1),
|
|
50
|
+
workflowName: z.string().min(1),
|
|
51
|
+
workflowUid: z.string().min(1).optional()
|
|
52
|
+
}).strict();
|
|
53
|
+
const commandScheduleOptionsSchema = z.object({
|
|
54
|
+
command: z.array(z.string().min(1)).min(1),
|
|
55
|
+
generatedAt: z.date().default(() => /* @__PURE__ */ new Date()),
|
|
56
|
+
scheduleId: scheduleIdSchema.optional(),
|
|
57
|
+
task: z.string().min(1)
|
|
58
|
+
}).strict();
|
|
59
|
+
async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
|
|
60
|
+
const { config, ...schemaOptions } = rawOptions;
|
|
61
|
+
const options = submitRunnerArgoWorkflowOptionsSchema.parse(schemaOptions);
|
|
62
|
+
const payload = runnerCommandPayloadSchema.parse(parseRunnerCommandPayload(options.payloadJson));
|
|
63
|
+
const compiled = compileScheduleArtifact(config, parseScheduleArtifact(options.scheduleYaml, "schedule.yaml"));
|
|
64
|
+
const payloadConfigMapName = `pipeline-payload-${randomBytes(6).toString("hex")}`;
|
|
65
|
+
const scheduleArtifactConfigMapName = `pipeline-schedule-${randomBytes(6).toString("hex")}`;
|
|
66
|
+
const taskDescriptorConfigMapName = `pipeline-task-descriptors-${randomBytes(6).toString("hex")}`;
|
|
67
|
+
if (payload.workflow.id !== compiled.workflowId) throw new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`);
|
|
68
|
+
const graph = compileArgoExecutionGraph(compiled.plan);
|
|
69
|
+
const labels = {
|
|
70
|
+
"pipeline.oisin.dev/project": payload.run.project,
|
|
71
|
+
"pipeline.oisin.dev/run-id": payload.run.id,
|
|
72
|
+
"pipeline.oisin.dev/source": "argo-workflow",
|
|
73
|
+
"pipeline.oisin.dev/workflow": compiled.workflowId
|
|
74
|
+
};
|
|
75
|
+
const workflow = buildRunnerArgoWorkflowManifest({
|
|
76
|
+
codexAuthSecretName: options.codexAuthSecretName,
|
|
77
|
+
eventAuthSecretKey: options.eventAuthSecretKey,
|
|
78
|
+
eventAuthSecretName: options.eventAuthSecretName,
|
|
79
|
+
generateName: options.generateName,
|
|
80
|
+
githubAuthSecretName: options.githubAuthSecretName,
|
|
81
|
+
image: options.image,
|
|
82
|
+
imagePullPolicy: options.imagePullPolicy,
|
|
83
|
+
imagePullSecretName: options.imagePullSecretName,
|
|
84
|
+
labels,
|
|
85
|
+
name: options.name,
|
|
86
|
+
namespace: options.namespace,
|
|
87
|
+
opencodeAuthSecretName: options.opencodeAuthSecretName,
|
|
88
|
+
orchestrator: options.orchestrator,
|
|
89
|
+
payloadConfigMapName,
|
|
90
|
+
plan: compiled.plan,
|
|
91
|
+
queueName: options.queueName,
|
|
92
|
+
scheduleConfigMapName: scheduleArtifactConfigMapName,
|
|
93
|
+
serviceAccountName: options.serviceAccountName,
|
|
94
|
+
taskDescriptorConfigMapName
|
|
95
|
+
});
|
|
96
|
+
const { coreApi, workflowApi } = apiClients(options, dependencies);
|
|
97
|
+
await assertArgoWorkflowCrdAvailable(workflowApi);
|
|
98
|
+
await coreApi.createNamespacedConfigMap({
|
|
99
|
+
body: configMapSchema.parse({
|
|
100
|
+
apiVersion: "v1",
|
|
101
|
+
data: { "payload.json": options.payloadJson },
|
|
102
|
+
kind: "ConfigMap",
|
|
103
|
+
metadata: {
|
|
104
|
+
labels,
|
|
105
|
+
name: payloadConfigMapName,
|
|
106
|
+
namespace: options.namespace
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
namespace: options.namespace
|
|
110
|
+
});
|
|
111
|
+
await coreApi.createNamespacedConfigMap({
|
|
112
|
+
body: configMapSchema.parse({
|
|
113
|
+
apiVersion: "v1",
|
|
114
|
+
data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
|
|
115
|
+
kind: "ConfigMap",
|
|
116
|
+
metadata: {
|
|
117
|
+
labels,
|
|
118
|
+
name: taskDescriptorConfigMapName,
|
|
119
|
+
namespace: options.namespace
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
namespace: options.namespace
|
|
123
|
+
});
|
|
124
|
+
await coreApi.createNamespacedConfigMap({
|
|
125
|
+
body: configMapSchema.parse({
|
|
126
|
+
apiVersion: "v1",
|
|
127
|
+
data: { "schedule.yaml": options.scheduleYaml },
|
|
128
|
+
kind: "ConfigMap",
|
|
129
|
+
metadata: {
|
|
130
|
+
labels,
|
|
131
|
+
name: scheduleArtifactConfigMapName,
|
|
132
|
+
namespace: options.namespace
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
namespace: options.namespace
|
|
136
|
+
});
|
|
137
|
+
const response = await workflowApi.createNamespacedCustomObject({
|
|
138
|
+
body: runnerArgoWorkflowManifestSchema.parse(workflow),
|
|
139
|
+
group: "argoproj.io",
|
|
140
|
+
namespace: options.namespace,
|
|
141
|
+
plural: "workflows",
|
|
142
|
+
version: "v1alpha1"
|
|
143
|
+
});
|
|
144
|
+
const created = z.object({ metadata: z.object({
|
|
145
|
+
name: z.string().min(1).optional(),
|
|
146
|
+
uid: z.string().min(1).optional()
|
|
147
|
+
}).passthrough() }).passthrough().parse(response);
|
|
148
|
+
return submitRunnerArgoWorkflowResultSchema.parse({
|
|
149
|
+
namespace: options.namespace,
|
|
150
|
+
payloadConfigMapName,
|
|
151
|
+
scheduleConfigMapName: scheduleArtifactConfigMapName,
|
|
152
|
+
taskDescriptorConfigMapName,
|
|
153
|
+
workflowName: created.metadata.name ?? workflow.metadata.name,
|
|
154
|
+
workflowUid: created.metadata.uid
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function assertArgoWorkflowCrdAvailable(workflowApi) {
|
|
158
|
+
try {
|
|
159
|
+
await workflowApi.getClusterCustomObject({
|
|
160
|
+
group: "apiextensions.k8s.io",
|
|
161
|
+
name: "workflows.argoproj.io",
|
|
162
|
+
plural: "customresourcedefinitions",
|
|
163
|
+
version: "v1"
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (isKubernetesNotFound(error)) throw new Error("Argo Workflows is not installed in the target cluster: missing CustomResourceDefinition workflows.argoproj.io");
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function isKubernetesNotFound(error) {
|
|
171
|
+
if (!(error instanceof Error)) return false;
|
|
172
|
+
return "code" in error && error.code === 404 || error.message.includes("404") || error.message.includes("NotFound");
|
|
173
|
+
}
|
|
174
|
+
function buildCommandScheduleYaml(rawOptions) {
|
|
175
|
+
const options = commandScheduleOptionsSchema.parse(rawOptions);
|
|
176
|
+
const scheduleId = options.scheduleId ?? `custom-${randomBytes(8).toString("hex")}`;
|
|
177
|
+
return stringify({
|
|
178
|
+
generated_at: options.generatedAt.toISOString(),
|
|
179
|
+
kind: "pipeline-schedule",
|
|
180
|
+
root_workflow: "root",
|
|
181
|
+
schedule_id: scheduleId,
|
|
182
|
+
source_entrypoint: "custom",
|
|
183
|
+
task: options.task,
|
|
184
|
+
version: 1,
|
|
185
|
+
workflows: { root: { nodes: [{
|
|
186
|
+
command: options.command,
|
|
187
|
+
id: "command",
|
|
188
|
+
kind: "command"
|
|
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
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export { buildCommandScheduleYaml, submitRunnerArgoWorkflow };
|