@oisincoveney/pipeline 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) 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 +1 -1
  7. package/dist/gates.js +6 -225
  8. package/dist/mcp/gateway-error.js +15 -0
  9. package/dist/mcp/gateway.js +119 -220
  10. package/dist/moka-global-config.js +20 -20
  11. package/dist/moka-submit.d.ts +1 -1
  12. package/dist/pipeline-runtime.js +580 -371
  13. package/dist/run-state/git-refs.js +124 -94
  14. package/dist/runner-command-contract.d.ts +2 -2
  15. package/dist/runner-event-sink.js +37 -69
  16. package/dist/runtime/agent-node/agent-node.js +214 -173
  17. package/dist/runtime/builtins/builtins.js +344 -57
  18. package/dist/runtime/changed-files/changed-files.js +15 -27
  19. package/dist/runtime/changed-files/index.js +2 -0
  20. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  21. package/dist/runtime/gates/gates.js +46 -28
  22. package/dist/runtime/hooks/hooks.js +74 -29
  23. package/dist/runtime/opencode-server.js +27 -21
  24. package/dist/runtime/opencode-session-executor.js +101 -44
  25. package/dist/runtime/parallel-node/parallel-node.js +24 -5
  26. package/dist/runtime/select-candidate/select-candidate.js +45 -29
  27. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  28. package/dist/runtime/services/command-executor-service.js +8 -0
  29. package/dist/runtime/services/config-io-service.js +42 -0
  30. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  31. package/dist/runtime/services/git-porcelain-service.js +38 -0
  32. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  33. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  34. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  35. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  36. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  37. package/dist/runtime/services/select-candidate-service.js +13 -0
  38. package/package.json +1 -1
@@ -1,14 +1,13 @@
1
+ import { GitPorcelainService, GitPorcelainServiceLive } from "../runtime/services/git-porcelain-service.js";
2
+ import { Effect } from "effect";
1
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
4
  import { dirname, resolve } from "node:path";
3
5
  import { tmpdir } from "node:os";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
6
  //#region src/run-state/git-refs.ts
7
7
  const DEFAULT_WORKSPACE_PATH = "/workspace";
8
8
  const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
9
9
  const WRITABLE_GIT_CREDENTIAL_STORE = resolve(tmpdir(), "pipeline-git-credentials");
10
10
  const SCP_LIKE_SSH_REMOTE_RE = /^[^@\s]+@[^:\s]+:.+/u;
11
- const execGit = promisify(execFile);
12
11
  let preparedBasicAuthCredentialStore;
13
12
  function runnerGitRefs(payload, nodeId) {
14
13
  const prefix = `refs/heads/pipeline/runs/${payload.run.id}/${payload.workflow.id}`;
@@ -19,101 +18,132 @@ function runnerGitRefs(payload, nodeId) {
19
18
  };
20
19
  }
21
20
  async function prepareRunnerGitWorkspace(payload, options = {}) {
22
- if (options.cwd) return resolve(options.cwd);
23
- const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
24
- mkdirSync(dirname(worktreePath), { recursive: true });
25
- await runGit(dirname(worktreePath), [
26
- "clone",
27
- "--no-tags",
28
- payload.repository.url,
29
- worktreePath
30
- ]);
31
- await runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
32
- return worktreePath;
21
+ return await Effect.runPromise(Effect.provide(prepareRunnerGitWorkspaceEffect(payload, options), GitPorcelainServiceLive));
22
+ }
23
+ function prepareRunnerGitWorkspaceEffect(payload, options) {
24
+ return Effect.gen(function* () {
25
+ if (options.cwd) return resolve(options.cwd);
26
+ const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
27
+ yield* Effect.sync(() => mkdirSync(dirname(worktreePath), { recursive: true }));
28
+ yield* runGit(dirname(worktreePath), [
29
+ "clone",
30
+ "--no-tags",
31
+ payload.repository.url,
32
+ worktreePath
33
+ ]);
34
+ yield* runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
35
+ return worktreePath;
36
+ });
33
37
  }
34
38
  async function mergeDependencyRefs(input) {
35
- await configureGitCommitter(input.worktreePath, input.committer);
36
- for (const nodeId of input.dependencyNodeIds) {
37
- const ref = runnerGitRefs(input.payload, nodeId).nodeRef;
38
- await runGit(input.worktreePath, [
39
+ return await Effect.runPromise(Effect.provide(mergeDependencyRefsEffect(input), GitPorcelainServiceLive));
40
+ }
41
+ function mergeDependencyRefsEffect(input) {
42
+ return Effect.gen(function* () {
43
+ yield* configureGitCommitter(input.worktreePath, input.committer);
44
+ yield* Effect.forEach(input.dependencyNodeIds, (nodeId) => mergeDependencyRef(input.worktreePath, runnerGitRefs(input.payload, nodeId).nodeRef));
45
+ });
46
+ }
47
+ function mergeDependencyRef(worktreePath, ref) {
48
+ return Effect.gen(function* () {
49
+ yield* runGit(worktreePath, [
39
50
  "fetch",
40
51
  "origin",
41
52
  ref
42
53
  ]);
43
- await runGit(input.worktreePath, [
54
+ yield* runGit(worktreePath, [
44
55
  "merge",
45
56
  "--no-ff",
46
57
  "--no-edit",
47
58
  "FETCH_HEAD"
48
59
  ]);
49
- }
60
+ });
50
61
  }
51
62
  async function commitAndPushNodeRef(input) {
52
- await commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
53
- const sha = (await runGit(input.worktreePath, ["rev-parse", "HEAD"])).trim();
54
- await runGit(input.worktreePath, [
55
- "push",
56
- "origin",
57
- `HEAD:${runnerGitRefs(input.payload, input.nodeId).nodeRef}`
58
- ]);
59
- return sha;
63
+ return await Effect.runPromise(Effect.provide(commitAndPushNodeRefEffect(input), GitPorcelainServiceLive));
64
+ }
65
+ function commitAndPushNodeRefEffect(input) {
66
+ return Effect.gen(function* () {
67
+ yield* commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
68
+ const sha = yield* headSha(input.worktreePath);
69
+ yield* runGit(input.worktreePath, [
70
+ "push",
71
+ "origin",
72
+ `HEAD:${runnerGitRefs(input.payload, input.nodeId).nodeRef}`
73
+ ]);
74
+ return sha;
75
+ });
60
76
  }
61
77
  async function promoteFinalRef(input) {
62
- await mergeDependencyRefs({
63
- committer: input.committer,
64
- dependencyNodeIds: input.sourceNodeIds,
65
- payload: input.payload,
66
- worktreePath: input.worktreePath
78
+ return await Effect.runPromise(Effect.provide(promoteFinalRefEffect(input), GitPorcelainServiceLive));
79
+ }
80
+ function promoteFinalRefEffect(input) {
81
+ return Effect.gen(function* () {
82
+ yield* mergeDependencyRefsEffect({
83
+ committer: input.committer,
84
+ dependencyNodeIds: input.sourceNodeIds,
85
+ payload: input.payload,
86
+ worktreePath: input.worktreePath
87
+ });
88
+ yield* commitChangesIfNeeded(input.worktreePath, "final", input.committer);
89
+ const sha = yield* headSha(input.worktreePath);
90
+ yield* runGit(input.worktreePath, [
91
+ "push",
92
+ "origin",
93
+ `HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
94
+ ]);
95
+ return sha;
96
+ });
97
+ }
98
+ function headSha(worktreePath) {
99
+ return Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
100
+ }
101
+ function commitChangesIfNeeded(worktreePath, nodeId, committer) {
102
+ return Effect.gen(function* () {
103
+ if ((yield* runGit(worktreePath, [
104
+ "status",
105
+ "--porcelain",
106
+ "--untracked-files=all"
107
+ ])).trim().length === 0) return;
108
+ yield* runGit(worktreePath, ["add", "--all"]);
109
+ yield* configureGitCommitter(worktreePath, committer);
110
+ yield* runGit(worktreePath, [
111
+ "commit",
112
+ "-m",
113
+ `pipeline: ${nodeId}`
114
+ ]);
115
+ });
116
+ }
117
+ function configureGitCommitter(worktreePath, committer) {
118
+ return Effect.gen(function* () {
119
+ yield* runGit(worktreePath, [
120
+ "config",
121
+ "--local",
122
+ "user.name",
123
+ committer.name
124
+ ]);
125
+ yield* runGit(worktreePath, [
126
+ "config",
127
+ "--local",
128
+ "user.email",
129
+ committer.email
130
+ ]);
67
131
  });
68
- await commitChangesIfNeeded(input.worktreePath, "final", input.committer);
69
- const sha = (await runGit(input.worktreePath, ["rev-parse", "HEAD"])).trim();
70
- await runGit(input.worktreePath, [
71
- "push",
72
- "origin",
73
- `HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
74
- ]);
75
- return sha;
76
- }
77
- async function commitChangesIfNeeded(worktreePath, nodeId, committer) {
78
- if ((await runGit(worktreePath, [
79
- "status",
80
- "--porcelain",
81
- "--untracked-files=all"
82
- ])).trim().length === 0) return;
83
- await runGit(worktreePath, ["add", "--all"]);
84
- await configureGitCommitter(worktreePath, committer);
85
- await runGit(worktreePath, [
86
- "commit",
87
- "-m",
88
- `pipeline: ${nodeId}`
89
- ]);
90
- }
91
- async function configureGitCommitter(worktreePath, committer) {
92
- await runGit(worktreePath, [
93
- "config",
94
- "--local",
95
- "user.name",
96
- committer.name
97
- ]);
98
- await runGit(worktreePath, [
99
- "config",
100
- "--local",
101
- "user.email",
102
- committer.email
103
- ]);
104
132
  }
105
133
  function runnerGitCommandArgs(args, remoteUrl) {
106
134
  return [...gitCredentialConfigArgs(remoteUrl), ...args];
107
135
  }
108
- async function runGit(cwd, args) {
109
- const remoteUrl = await remoteUrlFromGitArgs(cwd, args);
110
- assertSshCredentialsAvailable(remoteUrl);
111
- const { stdout } = await execGit("git", runnerGitCommandArgs(args, remoteUrl), {
112
- cwd,
113
- encoding: "utf8",
114
- env: runnerGitEnv(remoteUrl)
136
+ function runGit(cwd, args) {
137
+ return Effect.gen(function* () {
138
+ const remoteUrl = yield* remoteUrlFromGitArgs(cwd, args);
139
+ yield* Effect.try({
140
+ try: () => assertSshCredentialsAvailable(remoteUrl),
141
+ catch: (error) => error
142
+ });
143
+ const git = yield* GitPorcelainService;
144
+ const commandArgs = runnerGitCommandArgs(args, remoteUrl);
145
+ return yield* git.run(cwd, commandArgs, runnerGitEnv(remoteUrl));
115
146
  });
116
- return stdout;
117
147
  }
118
148
  function assertSshCredentialsAvailable(remoteUrl) {
119
149
  if (!(remoteUrl && isSshRemote(remoteUrl))) return;
@@ -227,12 +257,15 @@ function isOwnerReadOnly(path) {
227
257
  function readCredentialFile(path) {
228
258
  return readFileSync(path, "utf8").trim();
229
259
  }
230
- async function remoteUrlFromGitArgs(cwd, args) {
231
- const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
232
- if (literalRemoteUrl) return literalRemoteUrl;
233
- const remoteName = remoteNameFromGitArgs(args);
234
- if (!remoteName) return;
235
- return await gitRemoteUrl(cwd, remoteName);
260
+ function remoteUrlFromGitArgs(cwd, args) {
261
+ return Effect.gen(function* () {
262
+ const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
263
+ if (literalRemoteUrl) return literalRemoteUrl;
264
+ return yield* remoteUrlForName(cwd, remoteNameFromGitArgs(args));
265
+ });
266
+ }
267
+ function remoteUrlForName(cwd, remoteName) {
268
+ return remoteName ? gitRemoteUrl(cwd, remoteName) : Effect.succeed(void 0);
236
269
  }
237
270
  function literalRemoteUrlFromGitArgs(args) {
238
271
  if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
@@ -247,20 +280,17 @@ function remoteNameFromGitArgs(args) {
247
280
  if (remoteArg && !remoteArg.startsWith("-") && !isRemoteUrl(remoteArg)) return remoteArg;
248
281
  }
249
282
  }
250
- async function gitRemoteUrl(cwd, remoteName) {
251
- const { stdout } = await execGit("git", [
252
- "remote",
253
- "get-url",
254
- remoteName
255
- ], {
256
- cwd,
257
- encoding: "utf8",
258
- env: {
283
+ function gitRemoteUrl(cwd, remoteName) {
284
+ return Effect.gen(function* () {
285
+ return (yield* (yield* GitPorcelainService).run(cwd, [
286
+ "remote",
287
+ "get-url",
288
+ remoteName
289
+ ], {
259
290
  ...process.env,
260
291
  GIT_TERMINAL_PROMPT: "0"
261
- }
292
+ })).trim() || void 0;
262
293
  });
263
- return stdout.trim() || void 0;
264
294
  }
265
295
  function isRemoteUrl(value) {
266
296
  return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- quick: "quick";
47
46
  full: "full";
47
+ quick: "quick";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- quick: "quick";
108
107
  full: "full";
108
+ quick: "quick";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;
@@ -1,39 +1,14 @@
1
1
  import { mapRuntimeEventToRunnerEventRecords } from "./runner-command-contract.js";
2
- import { Data } from "effect";
3
- import ky, { isHTTPError } from "ky";
2
+ import { RunnerEventSinkHttpService, RunnerEventSinkHttpServiceLive } from "./runtime/services/runner-event-sink-http-service.js";
3
+ import { Effect } from "effect";
4
4
  //#region src/runner-event-sink.ts
5
5
  const DEFAULT_BATCH_SIZE = 50;
6
6
  const DEFAULT_MAX_RETRIES = 0;
7
7
  const DEFAULT_RETRY_DELAY_MS = 250;
8
- const RETRYABLE_STATUS_CODES = [
9
- 408,
10
- 429,
11
- 500,
12
- 501,
13
- 502,
14
- 503,
15
- 504,
16
- 505,
17
- 506,
18
- 507,
19
- 508,
20
- 509,
21
- 510,
22
- 511
23
- ];
24
- var EventSinkHttpError = class extends Data.TaggedError("EventSinkHttpError") {
25
- constructor(status, message) {
26
- super({
27
- status,
28
- message
29
- });
30
- }
31
- };
32
8
  function createRunnerEventSink(options) {
33
- const batchSize = Math.max(1, options.batchSize ?? DEFAULT_BATCH_SIZE);
34
- const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
35
- if (!fetchImpl) throw new Error("Runner event sink requires fetch support");
36
- if (!options.authToken.trim()) throw new Error("Runner event sink requires an auth token");
9
+ const batchSize = positiveOrDefault(options.batchSize, DEFAULT_BATCH_SIZE);
10
+ const fetchImpl = resolveFetch(options.fetch);
11
+ assertAuthToken(options.authToken);
37
12
  const queue = [];
38
13
  let flushChain = Promise.resolve();
39
14
  let nextSequence = 1;
@@ -46,15 +21,17 @@ function createRunnerEventSink(options) {
46
21
  sequence
47
22
  };
48
23
  };
49
- const flushQueue = async () => {
50
- while (queue.length > 0) {
24
+ const flushQueueEffect = () => Effect.gen(function* () {
25
+ const service = yield* RunnerEventSinkHttpService;
26
+ while (hasQueuedEvents(queue)) {
51
27
  const batch = queue.slice(0, batchSize);
52
- await postBatch(options, fetchImpl, batch);
28
+ yield* service.postBatch(postBatchRequest(options, fetchImpl, batch));
53
29
  queue.splice(0, batch.length);
54
30
  }
55
- };
31
+ });
32
+ const runFlush = () => Effect.runPromise(Effect.provide(flushQueueEffect(), RunnerEventSinkHttpServiceLive));
56
33
  const runSerializedFlush = () => {
57
- const nextFlush = flushChain.then(flushQueue, flushQueue);
34
+ const nextFlush = flushChain.then(runFlush, runFlush);
58
35
  flushChain = nextFlush.catch(() => void 0);
59
36
  return nextFlush;
60
37
  };
@@ -145,41 +122,32 @@ function createRunnerEventSink(options) {
145
122
  }
146
123
  };
147
124
  }
148
- async function postBatch(options, fetchImpl, events) {
149
- const maxRetries = Math.max(0, options.maxRetries ?? DEFAULT_MAX_RETRIES);
150
- const retryDelayMs = Math.max(0, options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
151
- try {
152
- await ky.post(options.url, {
153
- fetch: kyFetchAdapter(fetchImpl),
154
- headers: { [options.authHeader ?? "Authorization"]: `Bearer ${options.authToken}` },
155
- json: { events },
156
- retry: {
157
- delay: () => retryDelayMs,
158
- limit: maxRetries,
159
- methods: ["post"],
160
- retryOnTimeout: true,
161
- statusCodes: RETRYABLE_STATUS_CODES
162
- }
163
- });
164
- } catch (err) {
165
- if (isHTTPError(err)) {
166
- let data = "";
167
- if (typeof err.data === "string") data = err.data;
168
- else if (err.data !== void 0) data = JSON.stringify(err.data);
169
- throw new EventSinkHttpError(err.response.status, `Event sink responded with ${err.response.status}${data ? `: ${data}` : ""}`);
170
- }
171
- throw err instanceof Error ? err : new Error(String(err));
172
- }
125
+ function hasQueuedEvents(queue) {
126
+ return queue.length > 0;
173
127
  }
174
- function kyFetchAdapter(fetchImpl) {
175
- return async (input, init) => {
176
- const request = new Request(input, init);
177
- return fetchImpl(request.url, {
178
- body: await request.clone().text(),
179
- headers: request.headers,
180
- method: request.method,
181
- signal: request.signal
182
- });
128
+ function positiveOrDefault(value, fallback) {
129
+ return Math.max(1, value ?? fallback);
130
+ }
131
+ function nonNegativeOrDefault(value, fallback) {
132
+ return Math.max(0, value ?? fallback);
133
+ }
134
+ function resolveFetch(fetchImpl) {
135
+ const resolved = fetchImpl ?? globalThis.fetch?.bind(globalThis);
136
+ if (!resolved) throw new Error("Runner event sink requires fetch support");
137
+ return resolved;
138
+ }
139
+ function assertAuthToken(authToken) {
140
+ if (!authToken.trim()) throw new Error("Runner event sink requires an auth token");
141
+ }
142
+ function postBatchRequest(options, fetchImpl, events) {
143
+ return {
144
+ authHeader: options.authHeader,
145
+ authToken: options.authToken,
146
+ events,
147
+ fetch: fetchImpl,
148
+ maxRetries: nonNegativeOrDefault(options.maxRetries, DEFAULT_MAX_RETRIES),
149
+ retryDelayMs: nonNegativeOrDefault(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS),
150
+ url: options.url
183
151
  };
184
152
  }
185
153
  function timestamp(now) {