@oisincoveney/pipeline 1.16.2 → 1.18.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 +6 -7
- package/dist/commands/pipeline-command.js +28 -0
- package/dist/commands/runner-job-command.js +10 -0
- package/dist/config.d.ts +15 -1
- package/dist/config.js +23 -1
- package/dist/index.js +12 -42
- package/dist/install-commands.js +8 -7
- package/dist/mcp/gateway.js +17 -20
- package/dist/pipeline-init.js +103 -2
- package/dist/runner-event-sink.js +14 -0
- package/dist/runner-job/delivery.js +57 -0
- package/dist/runner-job/devspace.js +34 -0
- package/dist/runner-job/run.js +282 -0
- package/dist/runner-job/workspace.js +52 -0
- package/dist/runner-job-contract.d.ts +71 -81
- package/dist/runner-job-contract.js +31 -54
- package/dist/runner.d.ts +1 -3
- package/dist/runner.js +5 -31
- package/dist/runtime/agent-node/agent-node.js +1 -1
- package/dist/runtime/changed-files/changed-files.js +2 -2
- package/dist/runtime/context/context.js +1 -1
- package/dist/runtime/drain-merge/drain-merge.js +2 -2
- package/dist/runtime/worktrees/worktrees.js +4 -4
- package/dist/runtime-machines/workflow-machine.js +1 -1
- package/dist/schedule-planner.js +15 -3
- package/dist/toml.js +12 -0
- package/docs/config-architecture.md +1 -1
- package/docs/mcp-gateway.md +11 -15
- package/docs/mcp-host-isolation.md +8 -22
- package/docs/operator-guide.md +31 -20
- package/docs/pipeline-console-runner-contract.md +109 -37
- package/package.json +1 -1
- package/dist/kubernetes-runner.js +0 -152
- package/dist/mcp/launch-plan.js +0 -100
package/README.md
CHANGED
|
@@ -39,17 +39,16 @@ MCP is the Momokaya remote endpoint
|
|
|
39
39
|
|
|
40
40
|
The default GitHub MCP registration uses GitHub's official container in
|
|
41
41
|
read-only mode and reads `GITHUB_PERSONAL_ACCESS_TOKEN` from the environment.
|
|
42
|
-
The Momokaya
|
|
43
|
-
`
|
|
44
|
-
|
|
42
|
+
The Momokaya gateway endpoint is protected by Traefik HTTP Basic auth. Set
|
|
43
|
+
`PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
|
|
44
|
+
value before starting Codex or OpenCode:
|
|
45
45
|
|
|
46
46
|
```shell
|
|
47
|
-
export
|
|
47
|
+
export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' | base64)"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
registration for that private endpoint.
|
|
50
|
+
`pipe init` writes project-level Codex and OpenCode config that points at the
|
|
51
|
+
singleton `pipeline-gateway` MCP server.
|
|
53
52
|
|
|
54
53
|
Check local prerequisites and config health:
|
|
55
54
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/commands/pipeline-command.ts
|
|
2
|
+
const BUILTIN_PIPE_COMMANDS = new Set([
|
|
3
|
+
"run",
|
|
4
|
+
"pipe",
|
|
5
|
+
"validate",
|
|
6
|
+
"explain-plan",
|
|
7
|
+
"doctor",
|
|
8
|
+
"init",
|
|
9
|
+
"install-commands",
|
|
10
|
+
"mcp",
|
|
11
|
+
"runner-job"
|
|
12
|
+
]);
|
|
13
|
+
function registerConfiguredEntrypointCommands(program, config, runEntrypoint) {
|
|
14
|
+
const registered = /* @__PURE__ */ new Set();
|
|
15
|
+
if (!config) return registered;
|
|
16
|
+
const reservedCommands = new Set(program.commands.map((command) => command.name()));
|
|
17
|
+
for (const [id, entrypoint] of Object.entries(config.entrypoints)) {
|
|
18
|
+
if (reservedCommands.has(id)) continue;
|
|
19
|
+
program.command(id).description(entrypoint.description ?? `Run the ${id} workflow`).argument("<description...>", "task description").action(async (descriptionParts) => {
|
|
20
|
+
await runEntrypoint(id, descriptionParts.join(" "));
|
|
21
|
+
});
|
|
22
|
+
registered.add(id);
|
|
23
|
+
reservedCommands.add(id);
|
|
24
|
+
}
|
|
25
|
+
return registered;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { BUILTIN_PIPE_COMMANDS, registerConfiguredEntrypointCommands };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { runRunnerJob } from "../runner-job/run.js";
|
|
2
|
+
//#region src/commands/runner-job-command.ts
|
|
3
|
+
function registerRunnerJobCommand(program) {
|
|
4
|
+
program.command("runner-job").description("Run an in-pod pipeline runner job from the console payload").action(async () => {
|
|
5
|
+
const exitCode = await runRunnerJob();
|
|
6
|
+
process.exitCode = exitCode;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { registerRunnerJobCommand };
|
package/dist/config.d.ts
CHANGED
|
@@ -239,7 +239,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
239
239
|
local: "local";
|
|
240
240
|
}>;
|
|
241
241
|
provider: z.ZodLiteral<"toolhive">;
|
|
242
|
-
|
|
242
|
+
authorization_env: z.ZodDefault<z.ZodString>;
|
|
243
243
|
url: z.ZodOptional<z.ZodString>;
|
|
244
244
|
url_env: z.ZodDefault<z.ZodString>;
|
|
245
245
|
}, z.core.$strict>>;
|
|
@@ -309,6 +309,20 @@ declare const configSchema: z.ZodObject<{
|
|
|
309
309
|
task: "task";
|
|
310
310
|
}>>>;
|
|
311
311
|
}, z.core.$strict>>>;
|
|
312
|
+
runner_job: z.ZodDefault<z.ZodObject<{
|
|
313
|
+
environment: z.ZodDefault<z.ZodObject<{
|
|
314
|
+
setup: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
315
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
316
|
+
command: z.ZodString;
|
|
317
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
318
|
+
}, z.core.$strict>>>;
|
|
319
|
+
smoke: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
320
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
321
|
+
command: z.ZodString;
|
|
322
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
323
|
+
}, z.core.$strict>>>;
|
|
324
|
+
}, z.core.$strict>>;
|
|
325
|
+
}, z.core.$strict>>;
|
|
312
326
|
rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
313
327
|
path: z.ZodString;
|
|
314
328
|
}, z.core.$strict>>>;
|
package/dist/config.js
CHANGED
|
@@ -132,7 +132,7 @@ const mcpGatewaySchema = z.object({
|
|
|
132
132
|
default_profile: z.string().min(1).optional(),
|
|
133
133
|
mode: z.enum(["hosted", "local"]),
|
|
134
134
|
provider: z.literal("toolhive"),
|
|
135
|
-
|
|
135
|
+
authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
|
|
136
136
|
url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP gateway url must use http or https" }).optional(),
|
|
137
137
|
url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
|
|
138
138
|
}).strict();
|
|
@@ -361,6 +361,19 @@ const workflowSchema = z.object({
|
|
|
361
361
|
execution: workflowExecutionSchema.optional(),
|
|
362
362
|
nodes: z.array(workflowNodeSchema)
|
|
363
363
|
}).strict();
|
|
364
|
+
const runnerJobCommandSchema = z.object({
|
|
365
|
+
args: z.array(z.string()).default([]),
|
|
366
|
+
command: z.string().min(1),
|
|
367
|
+
required: z.boolean().default(true)
|
|
368
|
+
}).strict();
|
|
369
|
+
const runnerJobEnvironmentSchema = z.object({
|
|
370
|
+
setup: z.array(runnerJobCommandSchema).default([]),
|
|
371
|
+
smoke: z.array(runnerJobCommandSchema).default([])
|
|
372
|
+
}).strict();
|
|
373
|
+
const runnerJobConfigSchema = z.object({ environment: runnerJobEnvironmentSchema.default({
|
|
374
|
+
setup: [],
|
|
375
|
+
smoke: []
|
|
376
|
+
}) }).strict();
|
|
364
377
|
const runnersFileSchema = z.object({
|
|
365
378
|
runners: strictRecord(runnerSchema).default({}),
|
|
366
379
|
version: z.literal(1)
|
|
@@ -381,6 +394,10 @@ const pipelineFileSchema = z.object({
|
|
|
381
394
|
on: {}
|
|
382
395
|
}),
|
|
383
396
|
orchestrator: orchestratorSchema,
|
|
397
|
+
runner_job: runnerJobConfigSchema.default({ environment: {
|
|
398
|
+
setup: [],
|
|
399
|
+
smoke: []
|
|
400
|
+
} }),
|
|
384
401
|
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
385
402
|
task_context: taskContextResolverSchema.optional(),
|
|
386
403
|
workflows: strictRecord(workflowSchema).default({}),
|
|
@@ -397,6 +414,10 @@ const configSchema = z.object({
|
|
|
397
414
|
mcp_servers: strictRecord(mcpServerSchema).default({}),
|
|
398
415
|
orchestrator: orchestratorSchema,
|
|
399
416
|
profiles: strictRecord(profileSchema).default({}),
|
|
417
|
+
runner_job: runnerJobConfigSchema.default({ environment: {
|
|
418
|
+
setup: [],
|
|
419
|
+
smoke: []
|
|
420
|
+
} }),
|
|
400
421
|
rules: strictRecord(pathRefSchema).default({}),
|
|
401
422
|
runners: strictRecord(runnerSchema).default({}),
|
|
402
423
|
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
@@ -519,6 +540,7 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
519
540
|
mcp_servers: profiles.mcp_servers,
|
|
520
541
|
orchestrator: pipeline.orchestrator,
|
|
521
542
|
profiles: profiles.profiles,
|
|
543
|
+
runner_job: pipeline.runner_job,
|
|
522
544
|
rules: profiles.rules,
|
|
523
545
|
runners: runners.runners,
|
|
524
546
|
schedules: pipeline.schedules,
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { PipelineConfigError, loadPipelineConfig, tryLoadPipelineConfig } from "./config.js";
|
|
3
|
-
import {
|
|
4
|
-
import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.js";
|
|
3
|
+
import { BUILTIN_PIPE_COMMANDS, registerConfiguredEntrypointCommands } from "./commands/pipeline-command.js";
|
|
5
4
|
import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
|
|
6
5
|
import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "./runner.js";
|
|
6
|
+
import { compileWorkflowPlan } from "./workflow-planner.js";
|
|
7
7
|
import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js";
|
|
8
|
-
import { runKubernetesRunnerJob } from "./kubernetes-runner.js";
|
|
9
|
-
import { formatPipelineInitResult, initPipelineProject } from "./pipeline-init.js";
|
|
10
8
|
import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "./schedule-planner.js";
|
|
9
|
+
import { registerRunnerJobCommand } from "./commands/runner-job-command.js";
|
|
10
|
+
import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.js";
|
|
11
|
+
import { formatPipelineInitResult, initPipelineProject } from "./pipeline-init.js";
|
|
11
12
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
12
13
|
import { resolve } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -190,17 +191,6 @@ function truncateMiddle(text, maxLength) {
|
|
|
190
191
|
const keep = Math.floor((maxLength - 32) / 2);
|
|
191
192
|
return `${text.slice(0, keep)}\n... truncated ...\n${text.slice(-keep)}`;
|
|
192
193
|
}
|
|
193
|
-
const BUILTIN_PIPE_COMMANDS = new Set([
|
|
194
|
-
"run",
|
|
195
|
-
"pipe",
|
|
196
|
-
"validate",
|
|
197
|
-
"explain-plan",
|
|
198
|
-
"doctor",
|
|
199
|
-
"init",
|
|
200
|
-
"install-commands",
|
|
201
|
-
"mcp",
|
|
202
|
-
"runner-job"
|
|
203
|
-
]);
|
|
204
194
|
function createCliProgram() {
|
|
205
195
|
const configuredPipeline = tryLoadConfiguredEntrypoints(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
|
|
206
196
|
const program = new Command();
|
|
@@ -284,11 +274,8 @@ function createCliProgram() {
|
|
|
284
274
|
});
|
|
285
275
|
console.log(formatInstallCommandsResult(result));
|
|
286
276
|
});
|
|
287
|
-
program
|
|
288
|
-
|
|
289
|
-
process.exitCode = exitCode;
|
|
290
|
-
});
|
|
291
|
-
const configuredEntrypointCommands = registerConfiguredEntrypointCommands(program, configuredPipeline);
|
|
277
|
+
registerRunnerJobCommand(program);
|
|
278
|
+
const configuredEntrypointCommands = registerConfiguredEntrypointCommands(program, configuredPipeline, (entrypoint, task) => pipe(task, { entrypoint }));
|
|
292
279
|
if (configuredEntrypointCommands.size > 0) program.configureHelp({ subcommandTerm(command) {
|
|
293
280
|
if (configuredEntrypointCommands.has(command.name())) return command.name();
|
|
294
281
|
return Help.prototype.subcommandTerm.call(this, command);
|
|
@@ -303,20 +290,6 @@ function tryLoadConfiguredEntrypoints(cwd) {
|
|
|
303
290
|
throw err;
|
|
304
291
|
}
|
|
305
292
|
}
|
|
306
|
-
function registerConfiguredEntrypointCommands(program, config) {
|
|
307
|
-
const registered = /* @__PURE__ */ new Set();
|
|
308
|
-
if (!config) return registered;
|
|
309
|
-
const reservedCommands = new Set(program.commands.map((command) => command.name()));
|
|
310
|
-
for (const [id, entrypoint] of Object.entries(config.entrypoints)) {
|
|
311
|
-
if (reservedCommands.has(id)) continue;
|
|
312
|
-
program.command(id).description(entrypoint.description ?? `Run the ${id} workflow`).argument("<description...>", "task description").action(async (descriptionParts) => {
|
|
313
|
-
await pipe(descriptionParts.join(" "), { entrypoint: id });
|
|
314
|
-
});
|
|
315
|
-
registered.add(id);
|
|
316
|
-
reservedCommands.add(id);
|
|
317
|
-
}
|
|
318
|
-
return registered;
|
|
319
|
-
}
|
|
320
293
|
function parseGatewayHostScope(value) {
|
|
321
294
|
if (value === "project" || value === "global") return value;
|
|
322
295
|
throw new Error("scope must be project or global");
|
|
@@ -527,7 +500,6 @@ function formatWorkflowPlanNode(node, config, worktreePath) {
|
|
|
527
500
|
`kind=${node.kind}`,
|
|
528
501
|
`needs=${node.needs.join(",") || "none"}`,
|
|
529
502
|
launch ? `runner=${launch.runnerId}` : "",
|
|
530
|
-
launch ? `strategy=${launch.strategy}` : "",
|
|
531
503
|
node.gates?.length ? `gates=${node.gates.length}` : "gates=0",
|
|
532
504
|
node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none"
|
|
533
505
|
].filter(Boolean).join(" ");
|
|
@@ -542,14 +514,12 @@ function resolveWorkflowSelection(config, workflowId, entrypointId) {
|
|
|
542
514
|
}
|
|
543
515
|
function formatOrchestratorPlan(config, worktreePath) {
|
|
544
516
|
const orchestrator = config.profiles[config.orchestrator.profile];
|
|
545
|
-
const launch = createOrchestratorLaunchPlan(config, {
|
|
546
|
-
nodeId: "orchestrator",
|
|
547
|
-
prompt: "<task>",
|
|
548
|
-
worktreePath
|
|
549
|
-
});
|
|
550
517
|
return [
|
|
551
|
-
`Orchestrator: runner=${
|
|
552
|
-
|
|
518
|
+
`Orchestrator: runner=${createOrchestratorLaunchPlan(config, {
|
|
519
|
+
nodeId: "orchestrator",
|
|
520
|
+
prompt: "<task>",
|
|
521
|
+
worktreePath
|
|
522
|
+
}).runnerId}`,
|
|
553
523
|
orchestrator.model ? `model=${orchestrator.model}` : "",
|
|
554
524
|
formatList("rules", orchestrator.rules),
|
|
555
525
|
formatList("skills", orchestrator.skills),
|
package/dist/install-commands.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveFileReference } from "./path-refs.js";
|
|
2
2
|
import { loadPipelineConfig } from "./config.js";
|
|
3
|
+
import { renderCodexGatewayConfig } from "./mcp/gateway.js";
|
|
3
4
|
import { compileWorkflowPlan } from "./workflow-planner.js";
|
|
4
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join, relative } from "node:path";
|
|
@@ -173,6 +174,8 @@ function entrypointDispatchBlock(host, config, id, entrypoint) {
|
|
|
173
174
|
`Generate a schedule for entrypoint \`${id}\` and the user task.`,
|
|
174
175
|
`The schedule policy is \`${entrypoint.schedule}\`.`,
|
|
175
176
|
`Run \`pipe run --entrypoint ${id} <task description>\` to generate and execute the schedule artifact.`,
|
|
177
|
+
"The pipeline CLI runtime is the deterministic graph scheduler for scheduled entrypoints.",
|
|
178
|
+
"It launches configured Codex/OpenCode agent subprocesses as soon as their dependencies pass.",
|
|
176
179
|
"Use `pipe run --schedule <schedule.yaml>` only when rerunning an existing schedule artifact."
|
|
177
180
|
].join("\n");
|
|
178
181
|
}
|
|
@@ -358,7 +361,7 @@ function codexDefinitions(config, cwd) {
|
|
|
358
361
|
})),
|
|
359
362
|
projectAgentsMdDefinition(cwd, "codex"),
|
|
360
363
|
...nativeCodexProfiles.map(([id, profile]) => codexTomlAgentDefinition(config, cwd, id, profile)),
|
|
361
|
-
codexProjectConfigAgentDefinition()
|
|
364
|
+
codexProjectConfigAgentDefinition(config)
|
|
362
365
|
];
|
|
363
366
|
}
|
|
364
367
|
function projectAgentsMdDefinition(cwd, host) {
|
|
@@ -406,6 +409,7 @@ function codexTomlAgentDefinition(config, cwd, id, profile) {
|
|
|
406
409
|
};
|
|
407
410
|
const skillConfig = codexSkillConfig(config, cwd, profile);
|
|
408
411
|
const agentConfig = {
|
|
412
|
+
name: id,
|
|
409
413
|
description: profile.description ?? id,
|
|
410
414
|
...profileWithResolvedModel.model ? { model: profileWithResolvedModel.model } : {},
|
|
411
415
|
...profile.filesystem?.mode ? { sandbox_mode: profile.filesystem.mode } : {},
|
|
@@ -424,16 +428,12 @@ function codexTomlAgentDefinition(config, cwd, id, profile) {
|
|
|
424
428
|
].join("\n"),
|
|
425
429
|
skills: { config: skillConfig }
|
|
426
430
|
};
|
|
427
|
-
const mcpConfig = "mcp_servers" in agentConfig ? agentConfig : {
|
|
428
|
-
...agentConfig,
|
|
429
|
-
mcp_servers: {}
|
|
430
|
-
};
|
|
431
431
|
return {
|
|
432
432
|
content: [
|
|
433
433
|
"# Generated by @oisincoveney/pipeline.",
|
|
434
434
|
`${OWNER_YAML_MARKER_PREFIX}host=codex`,
|
|
435
435
|
"",
|
|
436
|
-
stringify(
|
|
436
|
+
stringify(agentConfig).trimEnd(),
|
|
437
437
|
""
|
|
438
438
|
].join("\n"),
|
|
439
439
|
host: "codex",
|
|
@@ -470,13 +470,14 @@ function codexSkillConfig(config, cwd, profile) {
|
|
|
470
470
|
}];
|
|
471
471
|
});
|
|
472
472
|
}
|
|
473
|
-
function codexProjectConfigAgentDefinition() {
|
|
473
|
+
function codexProjectConfigAgentDefinition(config) {
|
|
474
474
|
return {
|
|
475
475
|
block: {
|
|
476
476
|
end: CODEX_AGENT_CONFIG_END,
|
|
477
477
|
start: CODEX_AGENT_CONFIG_START
|
|
478
478
|
},
|
|
479
479
|
content: [
|
|
480
|
+
...config.mcp_gateway ? [renderCodexGatewayConfig(config).trimEnd(), ""] : [],
|
|
480
481
|
CODEX_AGENT_CONFIG_START,
|
|
481
482
|
stringify({ agents: { max_depth: 1 } }).trimEnd(),
|
|
482
483
|
CODEX_AGENT_CONFIG_END,
|
package/dist/mcp/gateway.js
CHANGED
|
@@ -5,8 +5,7 @@ import { execa } from "execa";
|
|
|
5
5
|
//#region src/mcp/gateway.ts
|
|
6
6
|
const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
|
|
7
7
|
const DEFAULT_LOCAL_GATEWAY_URL = "http://127.0.0.1:4483/mcp";
|
|
8
|
-
const
|
|
9
|
-
const LEGACY_CODEX_MCP_RE = /\[mcp_servers\./;
|
|
8
|
+
const LEGACY_CODEX_MCP_RE = /^\s*\[mcp_servers\.(?!pipeline-gateway(?:\]|\.env_http_headers\]))/m;
|
|
10
9
|
const LEGACY_OPENCODE_MCP_RE = /"mcp"\s*:\s*{(?!\s*"pipeline-gateway")/s;
|
|
11
10
|
const LEGACY_PIPELINE_MCP_RE = /path:\s*\.mcp\.json|uvx\s+mcpm|mcpm\s+run/;
|
|
12
11
|
var PipelineMcpGatewayError = class extends Error {
|
|
@@ -48,7 +47,7 @@ function renderGatewayConfig(config) {
|
|
|
48
47
|
`mode: ${gateway.mode}`,
|
|
49
48
|
gateway.url ? `url: ${gateway.url}` : "",
|
|
50
49
|
`url_env: ${gateway.url_env}`,
|
|
51
|
-
`
|
|
50
|
+
`authorization_env: ${gateway.authorization_env}`,
|
|
52
51
|
gateway.default_profile ? `default_profile: ${gateway.default_profile}` : "",
|
|
53
52
|
`resolved_url: ${gatewayUrl(gateway)}`
|
|
54
53
|
].filter(Boolean).join("\n");
|
|
@@ -57,10 +56,12 @@ function renderCodexGatewayConfig(config) {
|
|
|
57
56
|
const gateway = configuredGateway(config);
|
|
58
57
|
return [
|
|
59
58
|
"# Generated by @oisincoveney/pipeline.",
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
"",
|
|
60
|
+
`[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}]`,
|
|
61
|
+
`url = ${JSON.stringify(gatewayUrl(gateway))}`,
|
|
62
|
+
"",
|
|
63
|
+
`[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}.env_http_headers]`,
|
|
64
|
+
`Authorization = ${JSON.stringify(gateway.authorization_env)}`,
|
|
64
65
|
""
|
|
65
66
|
].join("\n");
|
|
66
67
|
}
|
|
@@ -71,6 +72,7 @@ function renderOpenCodeGatewayConfig(config) {
|
|
|
71
72
|
mcp: { [PIPELINE_GATEWAY_SERVER_ID]: {
|
|
72
73
|
enabled: true,
|
|
73
74
|
headers: gatewayOpenCodeHeaders(gateway),
|
|
75
|
+
oauth: false,
|
|
74
76
|
type: "remote",
|
|
75
77
|
url: gatewayUrl(gateway)
|
|
76
78
|
} }
|
|
@@ -164,23 +166,18 @@ function checkGatewayUrl(gateway) {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
function checkGatewayToken(gateway) {
|
|
167
|
-
return process.env[gateway.
|
|
168
|
-
detail: gateway.
|
|
169
|
-
name: "gateway-
|
|
169
|
+
return process.env[gateway.authorization_env] ? {
|
|
170
|
+
detail: gateway.authorization_env,
|
|
171
|
+
name: "gateway-authorization",
|
|
170
172
|
passed: true
|
|
171
173
|
} : {
|
|
172
|
-
detail: `Set ${gateway.
|
|
173
|
-
name: "gateway-
|
|
174
|
+
detail: `Set ${gateway.authorization_env}.`,
|
|
175
|
+
name: "gateway-authorization",
|
|
174
176
|
passed: false
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
179
|
function gatewayAuthorizationHeader(gateway) {
|
|
178
|
-
return `
|
|
179
|
-
}
|
|
180
|
-
function resolveHeaderEnvPlaceholders(value, env = process.env) {
|
|
181
|
-
return value.replace(AUTHORIZATION_ENV_RE, (_match, envVar) => {
|
|
182
|
-
return env[envVar] || `{env:${envVar}}`;
|
|
183
|
-
});
|
|
180
|
+
return `{env:${gateway.authorization_env}}`;
|
|
184
181
|
}
|
|
185
182
|
function gatewayOpenCodeHeaders(gateway) {
|
|
186
183
|
return { Authorization: gatewayAuthorizationHeader(gateway) };
|
|
@@ -254,7 +251,7 @@ async function checkGatewayHealth(gateway) {
|
|
|
254
251
|
}
|
|
255
252
|
}
|
|
256
253
|
async function firstHealthyGatewayResponse(url, gateway) {
|
|
257
|
-
const authorization = process.env[gateway.
|
|
254
|
+
const authorization = process.env[gateway.authorization_env];
|
|
258
255
|
for (const healthUrl of gatewayHealthUrls(url)) {
|
|
259
256
|
const response = await fetch(healthUrl, {
|
|
260
257
|
headers: {
|
|
@@ -302,4 +299,4 @@ function legacyContentHit(cwd, path, pattern) {
|
|
|
302
299
|
return pattern.test(readFileSync(fullPath, "utf8")) ? path : void 0;
|
|
303
300
|
}
|
|
304
301
|
//#endregion
|
|
305
|
-
export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus,
|
|
302
|
+
export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, renderCodexGatewayConfig, renderGatewayConfig, runGatewayDoctor, startLocalGateway };
|
package/dist/pipeline-init.js
CHANGED
|
@@ -287,10 +287,11 @@ runners:
|
|
|
287
287
|
opencode:
|
|
288
288
|
type: opencode
|
|
289
289
|
command: opencode
|
|
290
|
+
model: opencode/deepseek-v4-flash-free
|
|
290
291
|
capabilities:
|
|
291
292
|
native_subagents: true
|
|
292
293
|
rules: true
|
|
293
|
-
skills:
|
|
294
|
+
skills: true
|
|
294
295
|
mcp_servers: true
|
|
295
296
|
tools: [read, list, grep, glob, bash, edit, write, task]
|
|
296
297
|
filesystem: [read-only, workspace-write]
|
|
@@ -353,7 +354,7 @@ mcp_gateway:
|
|
|
353
354
|
provider: toolhive
|
|
354
355
|
mode: local
|
|
355
356
|
url_env: PIPELINE_MCP_GATEWAY_URL
|
|
356
|
-
|
|
357
|
+
authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
|
|
357
358
|
default_profile: default
|
|
358
359
|
|
|
359
360
|
profiles:
|
|
@@ -563,6 +564,106 @@ profiles:
|
|
|
563
564
|
repair:
|
|
564
565
|
enabled: true
|
|
565
566
|
max_attempts: 1
|
|
567
|
+
pipeline-opencode-researcher:
|
|
568
|
+
runner: opencode
|
|
569
|
+
description: Research the requested task and produce structured findings with OpenCode.
|
|
570
|
+
instructions:
|
|
571
|
+
path: .pipeline/prompts/researcher.md
|
|
572
|
+
rules: [test-first]
|
|
573
|
+
skills: [research, spec, scope]
|
|
574
|
+
mcp_servers: [pipeline-gateway]
|
|
575
|
+
tools: [read, list, grep, glob, bash]
|
|
576
|
+
filesystem:
|
|
577
|
+
mode: read-only
|
|
578
|
+
allow: ["**/*"]
|
|
579
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
580
|
+
network:
|
|
581
|
+
mode: inherit
|
|
582
|
+
output:
|
|
583
|
+
format: json_schema
|
|
584
|
+
schema_path: .pipeline/schemas/research.schema.json
|
|
585
|
+
repair:
|
|
586
|
+
enabled: true
|
|
587
|
+
max_attempts: 1
|
|
588
|
+
pipeline-opencode-test-writer:
|
|
589
|
+
runner: opencode
|
|
590
|
+
description: Add focused failing tests for the requested behavior with OpenCode.
|
|
591
|
+
instructions:
|
|
592
|
+
path: .pipeline/prompts/test-writer.md
|
|
593
|
+
rules: [test-first]
|
|
594
|
+
skills: [test]
|
|
595
|
+
mcp_servers: [pipeline-gateway]
|
|
596
|
+
tools: [read, list, grep, glob, bash, edit, write]
|
|
597
|
+
filesystem:
|
|
598
|
+
mode: workspace-write
|
|
599
|
+
allow: ["**/*"]
|
|
600
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
601
|
+
network:
|
|
602
|
+
mode: inherit
|
|
603
|
+
output:
|
|
604
|
+
format: text
|
|
605
|
+
pipeline-opencode-code-writer:
|
|
606
|
+
runner: opencode
|
|
607
|
+
scheduling_roles: [implementation]
|
|
608
|
+
description: Implement production code until the failing tests pass with OpenCode.
|
|
609
|
+
instructions:
|
|
610
|
+
path: .pipeline/prompts/code-writer.md
|
|
611
|
+
rules: [test-first]
|
|
612
|
+
skills: [trace, test, fix, library-first-development]
|
|
613
|
+
mcp_servers: [pipeline-gateway]
|
|
614
|
+
tools: [read, list, grep, glob, bash, edit, write]
|
|
615
|
+
filesystem:
|
|
616
|
+
mode: workspace-write
|
|
617
|
+
allow: ["**/*"]
|
|
618
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
619
|
+
network:
|
|
620
|
+
mode: inherit
|
|
621
|
+
output:
|
|
622
|
+
format: text
|
|
623
|
+
pipeline-opencode-acceptance-reviewer:
|
|
624
|
+
runner: opencode
|
|
625
|
+
scheduling_roles: [coverage]
|
|
626
|
+
description: Audit the finished change against every acceptance criterion with OpenCode.
|
|
627
|
+
instructions:
|
|
628
|
+
path: .pipeline/prompts/acceptance-reviewer.md
|
|
629
|
+
rules: [verification]
|
|
630
|
+
skills: [critique, doubt]
|
|
631
|
+
mcp_servers: [pipeline-gateway]
|
|
632
|
+
tools: [read, list, grep, glob, bash]
|
|
633
|
+
filesystem:
|
|
634
|
+
mode: read-only
|
|
635
|
+
allow: ["**/*"]
|
|
636
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
637
|
+
network:
|
|
638
|
+
mode: inherit
|
|
639
|
+
output:
|
|
640
|
+
format: json_schema
|
|
641
|
+
schema_path: .pipeline/schemas/acceptance.schema.json
|
|
642
|
+
repair:
|
|
643
|
+
enabled: true
|
|
644
|
+
max_attempts: 1
|
|
645
|
+
pipeline-opencode-verifier:
|
|
646
|
+
runner: opencode
|
|
647
|
+
scheduling_roles: [coverage]
|
|
648
|
+
description: Verify checks, implementation fit, and final evidence with OpenCode.
|
|
649
|
+
instructions:
|
|
650
|
+
path: .pipeline/prompts/verifier.md
|
|
651
|
+
rules: [verification]
|
|
652
|
+
skills: [verify, critique, secure, optimize]
|
|
653
|
+
mcp_servers: [pipeline-gateway]
|
|
654
|
+
tools: [read, list, grep, glob, bash]
|
|
655
|
+
filesystem:
|
|
656
|
+
mode: read-only
|
|
657
|
+
allow: ["**/*"]
|
|
658
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
659
|
+
network:
|
|
660
|
+
mode: inherit
|
|
661
|
+
output:
|
|
662
|
+
format: json_schema
|
|
663
|
+
schema_path: .pipeline/schemas/verify.schema.json
|
|
664
|
+
repair:
|
|
665
|
+
enabled: true
|
|
666
|
+
max_attempts: 1
|
|
566
667
|
`;
|
|
567
668
|
const VERDICT_SCHEMA = z.enum(["PASS", "FAIL"]);
|
|
568
669
|
const STRING_ARRAY_SCHEMA = z.array(z.string());
|
|
@@ -87,6 +87,20 @@ function createRunnerEventSink(options) {
|
|
|
87
87
|
workflowId
|
|
88
88
|
});
|
|
89
89
|
},
|
|
90
|
+
recordRunnerJobPhase(phase, message, output) {
|
|
91
|
+
queue.push({
|
|
92
|
+
...nextEnvelope(),
|
|
93
|
+
log: {
|
|
94
|
+
level: "info",
|
|
95
|
+
message,
|
|
96
|
+
output: {
|
|
97
|
+
phase,
|
|
98
|
+
...output
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
type: "runner.job.phase"
|
|
102
|
+
});
|
|
103
|
+
},
|
|
90
104
|
recordRuntimeEvent,
|
|
91
105
|
recordSchemaValidationFailure(message, issues, workflowId) {
|
|
92
106
|
queue.push({
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
//#region src/runner-job/delivery.ts
|
|
3
|
+
const GITHUB_HTTPS_REPOSITORY_RE = /^https:\/\/github\.com\/([^/]+\/[^/.]+)(?:\.git)?$/;
|
|
4
|
+
const GITHUB_SSH_REPOSITORY_RE = /^git@github\.com:([^/]+\/[^/.]+)(?:\.git)?$/;
|
|
5
|
+
const createPullRequest = async (options) => {
|
|
6
|
+
if (!options.payload.delivery.pullRequest) return null;
|
|
7
|
+
const repository = options.payload.repository;
|
|
8
|
+
const env = compactEnv(options.env);
|
|
9
|
+
const runCommand = options.runCommand ?? runDeliveryCommand;
|
|
10
|
+
const branch = (await runCommand("git", ["branch", "--show-current"], {
|
|
11
|
+
cwd: options.worktreePath,
|
|
12
|
+
env,
|
|
13
|
+
stdin: "ignore"
|
|
14
|
+
})).stdout.trim();
|
|
15
|
+
if (!branch) throw new Error("Pull request delivery requires a checked-out branch");
|
|
16
|
+
const headOwner = options.env.PIPELINE_PR_HEAD_OWNER?.trim() || "oisin-bot";
|
|
17
|
+
const repositoryName = githubRepositoryName(repository.url);
|
|
18
|
+
await runCommand("git", [
|
|
19
|
+
"push",
|
|
20
|
+
"--set-upstream",
|
|
21
|
+
"origin",
|
|
22
|
+
branch
|
|
23
|
+
], {
|
|
24
|
+
cwd: options.worktreePath,
|
|
25
|
+
env,
|
|
26
|
+
stdin: "ignore"
|
|
27
|
+
});
|
|
28
|
+
const url = (await runCommand("gh", [
|
|
29
|
+
"pr",
|
|
30
|
+
"create",
|
|
31
|
+
"--fill",
|
|
32
|
+
"--base",
|
|
33
|
+
repository.baseBranch,
|
|
34
|
+
"--head",
|
|
35
|
+
`${headOwner}:${branch}`,
|
|
36
|
+
"--repo",
|
|
37
|
+
repositoryName
|
|
38
|
+
], {
|
|
39
|
+
cwd: options.worktreePath,
|
|
40
|
+
env,
|
|
41
|
+
stdin: "ignore"
|
|
42
|
+
})).stdout.trim();
|
|
43
|
+
return url ? { url } : null;
|
|
44
|
+
};
|
|
45
|
+
const runDeliveryCommand = (command, args, options) => execa(command, args, options);
|
|
46
|
+
function compactEnv(env) {
|
|
47
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => Boolean(entry[1])));
|
|
48
|
+
}
|
|
49
|
+
function githubRepositoryName(repositoryUrl) {
|
|
50
|
+
const httpsMatch = repositoryUrl.match(GITHUB_HTTPS_REPOSITORY_RE);
|
|
51
|
+
if (httpsMatch?.[1]) return httpsMatch[1];
|
|
52
|
+
const sshMatch = repositoryUrl.match(GITHUB_SSH_REPOSITORY_RE);
|
|
53
|
+
if (sshMatch?.[1]) return sshMatch[1];
|
|
54
|
+
throw new Error("Pull request delivery requires a GitHub repository URL");
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { createPullRequest };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { loadPipelineConfig } from "../config.js";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
//#region src/runner-job/devspace.ts
|
|
4
|
+
function assertRunnerDevspaceReady(worktreePath) {
|
|
5
|
+
return { config: loadPipelineConfig(worktreePath, { allowMissingLintFileReferences: true }) };
|
|
6
|
+
}
|
|
7
|
+
async function runRunnerDevspaceSmoke(options) {
|
|
8
|
+
const smoke = options.config.runner_job.environment.smoke;
|
|
9
|
+
if (smoke.length === 0) return "skipped";
|
|
10
|
+
const runCommand = options.runCommand ?? runDevspaceCommand;
|
|
11
|
+
for (const command of smoke) await runCommand(command.command, command.args, {
|
|
12
|
+
cwd: options.worktreePath,
|
|
13
|
+
env: compactEnv(options.env),
|
|
14
|
+
stdin: "ignore"
|
|
15
|
+
});
|
|
16
|
+
return "ran";
|
|
17
|
+
}
|
|
18
|
+
async function runRunnerEnvironmentSetup(options) {
|
|
19
|
+
const setup = options.config.runner_job.environment.setup;
|
|
20
|
+
if (setup.length === 0) return "skipped";
|
|
21
|
+
const runCommand = options.runCommand ?? runDevspaceCommand;
|
|
22
|
+
for (const command of setup) await runCommand(command.command, command.args, {
|
|
23
|
+
cwd: options.worktreePath,
|
|
24
|
+
env: compactEnv(options.env),
|
|
25
|
+
stdin: "ignore"
|
|
26
|
+
});
|
|
27
|
+
return "ran";
|
|
28
|
+
}
|
|
29
|
+
const runDevspaceCommand = (command, args, options) => execa(command, args, options);
|
|
30
|
+
function compactEnv(env) {
|
|
31
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => Boolean(entry[1])));
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { assertRunnerDevspaceReady, runRunnerDevspaceSmoke, runRunnerEnvironmentSetup };
|