@oisincoveney/pipeline 1.17.0 → 1.18.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.
@@ -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
@@ -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
@@ -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/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- pass: "pass";
17
16
  fail: "fail";
17
+ pass: "pass";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
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 { BUILTIN_PIPE_COMMANDS, registerConfiguredEntrypointCommands } from "./commands/pipeline-command.js";
3
4
  import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
4
- import { compileWorkflowPlan } from "./workflow-planner.js";
5
- import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.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.command("runner-job").description("Run an in-pod pipeline runner job from the console payload").action(async () => {
288
- const exitCode = await runKubernetesRunnerJob();
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");
@@ -72,6 +72,7 @@ function renderOpenCodeGatewayConfig(config) {
72
72
  mcp: { [PIPELINE_GATEWAY_SERVER_ID]: {
73
73
  enabled: true,
74
74
  headers: gatewayOpenCodeHeaders(gateway),
75
+ oauth: false,
75
76
  type: "remote",
76
77
  url: gatewayUrl(gateway)
77
78
  } }
@@ -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 };
@@ -0,0 +1,279 @@
1
+ import { PipelineConfigError } from "../config.js";
2
+ import { runPipelineFromConfig } from "../pipeline-runtime.js";
3
+ import { RUNNER_PAYLOAD_ENV, parseRunnerJobPayloadWithIssues } from "../runner-job-contract.js";
4
+ import { createRunnerEventSink } from "../runner-event-sink.js";
5
+ import { compileScheduleArtifact, generateScheduleArtifact } from "../schedule-planner.js";
6
+ import { createPullRequest } from "./delivery.js";
7
+ import { assertRunnerDevspaceReady, runRunnerDevspaceSmoke, runRunnerEnvironmentSetup } from "./devspace.js";
8
+ import { prepareRunnerWorkspace } from "./workspace.js";
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ //#region src/runner-job/run.ts
12
+ const EXIT_PASS = 0;
13
+ const EXIT_FAIL = 1;
14
+ const EXIT_CANCELLED = 130;
15
+ const EXIT_VALIDATION = 64;
16
+ const EXIT_STARTUP = 70;
17
+ const RUNNER_SCHEDULE_ENTRYPOINT = "pipe";
18
+ async function runRunnerJob(options = {}) {
19
+ const prepared = prepareRunnerJob(options);
20
+ if (prepared.exitCode !== void 0) return prepared.exitCode;
21
+ if ("validationFailure" in prepared) return reportPreparedValidationFailure(prepared.validationFailure, options);
22
+ const { env, payload, stderr } = prepared.job;
23
+ const controller = new AbortController();
24
+ const signalEmitter = options.signalEmitter ?? process;
25
+ const forceExit = options.onForceExit ?? ((exitCode) => process.exit(exitCode));
26
+ let signalExitCode;
27
+ let signalCount = 0;
28
+ let signalFinalResultRecorded = false;
29
+ const sink = createRunnerSink({
30
+ env,
31
+ events: payload.events,
32
+ fetch: options.fetch,
33
+ runId: payload.run.id
34
+ });
35
+ const handleSignal = (exitCode) => {
36
+ signalCount += 1;
37
+ if (signalCount === 1) {
38
+ signalExitCode = exitCode;
39
+ sink.recordCancellation(RUNNER_SCHEDULE_ENTRYPOINT);
40
+ signalFinalResultRecorded = true;
41
+ controller.abort();
42
+ return;
43
+ }
44
+ forceExit(signalExitCode ?? exitCode);
45
+ };
46
+ const handleSigterm = () => handleSignal(EXIT_CANCELLED);
47
+ const handleSigint = () => handleSignal(EXIT_CANCELLED);
48
+ signalEmitter.on("SIGTERM", handleSigterm);
49
+ signalEmitter.on("SIGINT", handleSigint);
50
+ const runner = options.pipelineRunner ?? runPipelineFromConfig;
51
+ let sawWorkflowFinish = false;
52
+ try {
53
+ const { config, readiness, task, workflowId, workspace } = await prepareReadyWorkspace(options, payload, env, sink);
54
+ const result = await runner({
55
+ config,
56
+ reporter: (event) => {
57
+ if (event.type === "workflow.finish") sawWorkflowFinish = true;
58
+ sink.recordRuntimeEvent(event);
59
+ },
60
+ runId: payload.run.id,
61
+ signal: controller.signal,
62
+ task,
63
+ hookPolicy: { allowCommandHooks: true },
64
+ workflowId,
65
+ worktreePath: workspace.worktreePath
66
+ });
67
+ if (result.outcome === "PASS") await deliverSuccessfulRun(options, payload, workspace, readiness, sink);
68
+ if (!(sawWorkflowFinish || signalFinalResultRecorded)) sink.recordFinalResult(result.outcome, result.plan.workflowId);
69
+ if (await flushAndReport(sink.flush, stderr) && !signalExitCode) return EXIT_STARTUP;
70
+ return signalExitCode ?? exitCodeForRuntimeResult(result);
71
+ } catch (err) {
72
+ const message = err instanceof Error ? err.message : String(err);
73
+ stderr.write(`${message}\n`);
74
+ await flushAndReport(sink.flush, stderr);
75
+ if (err instanceof PipelineConfigError) return signalExitCode ?? EXIT_VALIDATION;
76
+ return signalExitCode ?? EXIT_STARTUP;
77
+ } finally {
78
+ removeSignalListener(signalEmitter, "SIGTERM", handleSigterm);
79
+ removeSignalListener(signalEmitter, "SIGINT", handleSigint);
80
+ }
81
+ }
82
+ async function prepareReadyWorkspace(options, payload, env, sink) {
83
+ const workspace = await prepareWorkspace(options, payload, env);
84
+ sink.recordRunnerJobPhase("workspace.prepared", "runner workspace prepared", { worktreePath: workspace.worktreePath });
85
+ const readiness = assertRunnerDevspaceReady(workspace.worktreePath);
86
+ const config = requireRunnerConfig(readiness);
87
+ sink.recordRunnerJobPhase("environment.ready", "runner environment ready");
88
+ const setupStatus = await runRunnerEnvironmentSetup({
89
+ config,
90
+ env: workspace.env,
91
+ runCommand: options.runDevspaceCommand,
92
+ worktreePath: workspace.worktreePath
93
+ });
94
+ sink.recordRunnerJobPhase(`environment.setup.${setupStatus}`, `runner environment setup ${setupStatus}`);
95
+ const task = resolveRunnerTask(payload.task, workspace.worktreePath);
96
+ const compiled = await (options.prepareSchedule ?? (options.pipelineRunner ? useConfiguredDefaultWorkflow : generateRunnerSchedule))(config, task, workspace, sink);
97
+ return {
98
+ config: compiled.config,
99
+ readiness,
100
+ task,
101
+ workflowId: compiled.workflowId,
102
+ workspace
103
+ };
104
+ }
105
+ function requireRunnerConfig(readiness) {
106
+ if (!readiness.config) throw new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", "Runner jobs require a repository pipeline config", [{
107
+ message: ".pipeline/pipeline.yaml is required for runner jobs",
108
+ path: ".pipeline/pipeline.yaml"
109
+ }]);
110
+ return readiness.config;
111
+ }
112
+ function resolveRunnerTask(task, worktreePath) {
113
+ if (task.kind === "prompt") return task.prompt;
114
+ if (task.path) return readFileSync(join(worktreePath, task.path), "utf8");
115
+ return task.id;
116
+ }
117
+ function useConfiguredDefaultWorkflow(config) {
118
+ return Promise.resolve({
119
+ config,
120
+ workflowId: config.default_workflow
121
+ });
122
+ }
123
+ async function generateRunnerSchedule(config, task, workspace, sink) {
124
+ const result = await generateScheduleArtifact({
125
+ config,
126
+ entrypointId: RUNNER_SCHEDULE_ENTRYPOINT,
127
+ task,
128
+ worktreePath: workspace.worktreePath
129
+ });
130
+ sink.recordRunnerJobPhase("schedule.generated", "runner schedule generated", { path: result.path });
131
+ const compiled = compileScheduleArtifact(config, result.artifact, workspace.worktreePath);
132
+ return {
133
+ config: compiled.config,
134
+ workflowId: compiled.workflowId
135
+ };
136
+ }
137
+ async function deliverSuccessfulRun(options, payload, workspace, readiness, sink) {
138
+ if (readiness.config) {
139
+ const config = readiness.config;
140
+ const smokeStatus = await runRunnerDevspaceSmokeWithPhase(options, config, workspace, sink);
141
+ sink.recordRunnerJobPhase(`environment.smoke.${smokeStatus}`, `runner environment smoke ${smokeStatus}`);
142
+ }
143
+ const pullRequest = await createRunnerPullRequestWithPhase(options, payload, workspace, sink);
144
+ if (pullRequest) sink.recordRunnerJobPhase("delivery.pull_request", "runner pull request created", { url: pullRequest.url });
145
+ }
146
+ async function runRunnerDevspaceSmokeWithPhase(options, config, workspace, sink) {
147
+ try {
148
+ return await runRunnerDevspaceSmoke({
149
+ config,
150
+ env: workspace.env,
151
+ runCommand: options.runDevspaceCommand,
152
+ worktreePath: workspace.worktreePath
153
+ });
154
+ } catch (err) {
155
+ sink.recordRunnerJobPhase("environment.smoke.failed", "runner environment smoke failed", { error: errorMessage(err) });
156
+ throw err;
157
+ }
158
+ }
159
+ async function createRunnerPullRequestWithPhase(options, payload, workspace, sink) {
160
+ try {
161
+ return await createRunnerPullRequest(options, payload, workspace.worktreePath, workspace.env);
162
+ } catch (err) {
163
+ sink.recordRunnerJobPhase("delivery.pull_request.failed", "runner pull request creation failed", { error: errorMessage(err) });
164
+ throw err;
165
+ }
166
+ }
167
+ function createRunnerPullRequest(options, payload, worktreePath, env) {
168
+ return (options.createPullRequest ?? createPullRequest)({
169
+ env,
170
+ payload,
171
+ worktreePath
172
+ });
173
+ }
174
+ function prepareWorkspace(options, payload, env) {
175
+ return (options.prepareWorkspace ?? prepareRunnerWorkspace)({
176
+ cwd: options.cwd,
177
+ env,
178
+ payload
179
+ });
180
+ }
181
+ function prepareRunnerJob(options) {
182
+ const env = options.env ?? process.env;
183
+ const stderr = options.stderr ?? process.stderr;
184
+ const payloadRaw = env[RUNNER_PAYLOAD_ENV];
185
+ if (!payloadRaw) {
186
+ stderr.write(`${RUNNER_PAYLOAD_ENV} is required\n`);
187
+ return { exitCode: EXIT_VALIDATION };
188
+ }
189
+ const payload = parsePayload(payloadRaw, stderr);
190
+ if (!payload.ok) return { validationFailure: {
191
+ env,
192
+ error: payload.error,
193
+ recoverable: payload.recoverable,
194
+ stderr
195
+ } };
196
+ return { job: {
197
+ env,
198
+ payload: payload.payload,
199
+ stderr
200
+ } };
201
+ }
202
+ function parsePayload(payloadRaw, stderr) {
203
+ const result = parseRunnerJobPayloadWithIssues(payloadRaw);
204
+ if (!result.ok) stderr.write(`${result.error.message}\n`);
205
+ return result;
206
+ }
207
+ async function reportPreparedValidationFailure(failure, options) {
208
+ if (failure.recoverable) {
209
+ const sink = createRunnerSink({
210
+ env: failure.env,
211
+ events: failure.recoverable.events,
212
+ fetch: options.fetch,
213
+ runId: failure.recoverable.run.id
214
+ });
215
+ sink.recordSchemaValidationFailure(failure.error.message, failure.error.issues, RUNNER_SCHEDULE_ENTRYPOINT);
216
+ await flushAndReport(sink.flush, failure.stderr);
217
+ }
218
+ return EXIT_VALIDATION;
219
+ }
220
+ function resolveAuthToken(authTokenEnv, env, stderr) {
221
+ const token = env[authTokenEnv]?.trim();
222
+ if (!token) {
223
+ stderr.write(`Runner event auth token is required. Set ${authTokenEnv}.\n`);
224
+ return null;
225
+ }
226
+ return token;
227
+ }
228
+ function createRunnerSink(options) {
229
+ const authToken = resolveAuthToken(options.events.authTokenEnv, options.env, { write: () => true });
230
+ if (!authToken) return createNoopRunnerEventSink();
231
+ return createRunnerEventSink({
232
+ authHeader: options.events.authHeader,
233
+ authToken,
234
+ fetch: options.fetch,
235
+ runId: options.runId,
236
+ url: options.events.url
237
+ });
238
+ }
239
+ function createNoopRunnerEventSink() {
240
+ return {
241
+ fail: () => Promise.resolve(),
242
+ flush: () => Promise.resolve(),
243
+ recordCancellation: () => void 0,
244
+ recordFinalResult: () => void 0,
245
+ recordRunnerJobPhase: () => void 0,
246
+ recordRuntimeEvent: () => void 0,
247
+ recordSchemaValidationFailure: () => void 0
248
+ };
249
+ }
250
+ function errorMessage(err) {
251
+ return err instanceof Error ? err.message : String(err);
252
+ }
253
+ function exitCodeForRuntimeResult(result) {
254
+ switch (result.outcome) {
255
+ case "PASS": return EXIT_PASS;
256
+ case "FAIL": return EXIT_FAIL;
257
+ case "CANCELLED": return EXIT_CANCELLED;
258
+ default: return EXIT_STARTUP;
259
+ }
260
+ }
261
+ async function flushAndReport(flush, stderr) {
262
+ try {
263
+ await flush();
264
+ return false;
265
+ } catch (err) {
266
+ const message = err instanceof Error ? err.message : String(err);
267
+ stderr.write(`Event sink flush failed: ${message}\n`);
268
+ return true;
269
+ }
270
+ }
271
+ function removeSignalListener(emitter, event, listener) {
272
+ if (emitter.off) {
273
+ emitter.off(event, listener);
274
+ return;
275
+ }
276
+ emitter.removeListener?.(event, listener);
277
+ }
278
+ //#endregion
279
+ export { runRunnerJob };
@@ -0,0 +1,52 @@
1
+ import { simpleGit } from "simple-git";
2
+ //#region src/runner-job/workspace.ts
3
+ const RUNNER_JOB_WORKSPACE_PATH = "/workspace";
4
+ async function prepareRunnerWorkspace(options) {
5
+ const existingWorktreePath = options.env.PIPELINE_TARGET_PATH ?? options.cwd;
6
+ if (existingWorktreePath) return {
7
+ env: {
8
+ ...options.env,
9
+ PIPELINE_TARGET_PATH: existingWorktreePath
10
+ },
11
+ worktreePath: existingWorktreePath
12
+ };
13
+ const repository = options.payload.repository;
14
+ if (!repository) throw new RunnerWorkspaceError("repository is required for runner jobs");
15
+ const git = options.createGitClient?.() ?? simpleGit();
16
+ const repositoryUrl = repository.url;
17
+ const branchName = runnerBranchName(options.payload);
18
+ try {
19
+ await git.clone(repositoryUrl, RUNNER_JOB_WORKSPACE_PATH, ["--no-tags"]);
20
+ await git.cwd(RUNNER_JOB_WORKSPACE_PATH).checkoutBranch(branchName, repository.sha ?? `origin/${repository.baseBranch}`);
21
+ } catch (err) {
22
+ throw new RunnerWorkspaceError(redactSecretText(errorMessage(err)));
23
+ }
24
+ return {
25
+ env: {
26
+ ...options.env,
27
+ PIPELINE_TARGET_PATH: RUNNER_JOB_WORKSPACE_PATH
28
+ },
29
+ worktreePath: RUNNER_JOB_WORKSPACE_PATH
30
+ };
31
+ }
32
+ var RunnerWorkspaceError = class extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "RunnerWorkspaceError";
36
+ }
37
+ };
38
+ function errorMessage(err) {
39
+ return err instanceof Error ? err.message : String(err);
40
+ }
41
+ const CREDENTIAL_IN_URL_RE = /(https?:\/\/)([^:@\s/]+):([^@\s/]+)@/g;
42
+ function redactSecretText(value) {
43
+ return value.replace(CREDENTIAL_IN_URL_RE, "$1$2:<redacted>@");
44
+ }
45
+ function runnerBranchName(payload) {
46
+ const runId = payload.run?.id;
47
+ const source = payload.task?.kind === "ticket" ? payload.task.id : runId;
48
+ if (!(source && runId)) throw new RunnerWorkspaceError("run and task context are required for runner jobs");
49
+ return `pipeline/${source.trim().toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^[./-]+|[./-]+$/g, "").replace(/\.{2,}/g, ".") || runId}`;
50
+ }
51
+ //#endregion
52
+ export { prepareRunnerWorkspace };