@oisincoveney/pipeline 1.19.1 → 1.20.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 CHANGED
@@ -124,8 +124,11 @@ storage, Kueue discovery, and UI rendering. This package owns the in-container
124
124
  translation, authenticated event posting, signal cancellation, and final event
125
125
  flushing.
126
126
 
127
- The console starts the image with `OISIN_PIPELINE_RUNNER_PAYLOAD_JSON` and the
128
- runner-side event token. The payload contract is documented in
127
+ The console starts the image with the payload as a mounted ConfigMap file and
128
+ the event auth token as a mounted Secret file. The runner reads the payload
129
+ from `--payload-file` and reads the event auth token from the file path
130
+ specified in `events.authTokenFile`. No environment variables are used for
131
+ payload or auth token delivery. The payload contract is documented in
129
132
  [`docs/pipeline-console-runner-contract.md`](docs/pipeline-console-runner-contract.md).
130
133
  The executable contract is exported from
131
134
  `@oisincoveney/pipeline/runner-job-contract` for payload construction,
@@ -410,3 +413,6 @@ bun run check
410
413
  bun run test
411
414
  bun run build:cli
412
415
  ```
416
+
417
+
418
+ Runner Kubernetes Jobs can be generated with `buildRunnerJobK8sManifest` from `@oisincoveney/pipeline/runner-job-k8s`.
@@ -1,8 +1,12 @@
1
1
  import { runRunnerJob } from "../runner-job/run.js";
2
2
  //#region src/commands/runner-job-command.ts
3
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();
4
+ program.command("runner-job").description("Run an in-pod pipeline runner job from the console payload").option("--payload-file <path>", "Path to the runner job payload JSON file").option("--orchestrator <name>", "Orchestrator runner (codex|opencode)").argument("[orchestrator]", "Orchestrator runner (codex|opencode)").action(async (orchestratorArg, options) => {
5
+ const orchestrator = orchestratorArg ?? options.orchestrator;
6
+ const exitCode = await runRunnerJob({
7
+ payloadFile: options.payloadFile,
8
+ orchestrator
9
+ });
6
10
  process.exitCode = exitCode;
7
11
  });
8
12
  }
package/dist/config.d.ts CHANGED
@@ -10,6 +10,10 @@ declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "work
10
10
  declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
11
11
  declare const SCHEDULE_BASELINES: readonly ["epic", "pipe"];
12
12
  declare const SCHEDULING_ROLES: readonly ["coverage", "implementation"];
13
+ declare const DEFAULT_RUNNER_JOB_GIT_COMMITTER: {
14
+ readonly email: "git@oisin.ee";
15
+ readonly name: "oisin-bot";
16
+ };
13
17
  type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
14
18
  interface PipelineConfigIssue {
15
19
  message: string;
@@ -298,6 +302,7 @@ declare const configSchema: z.ZodObject<{
298
302
  implementation: "implementation";
299
303
  }>>>;
300
304
  skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
305
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
301
306
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
302
307
  read: "read";
303
308
  list: "list";
@@ -322,6 +327,12 @@ declare const configSchema: z.ZodObject<{
322
327
  required: z.ZodDefault<z.ZodBoolean>;
323
328
  }, z.core.$strict>>>;
324
329
  }, z.core.$strict>>;
330
+ git: z.ZodDefault<z.ZodObject<{
331
+ committer: z.ZodDefault<z.ZodObject<{
332
+ email: z.ZodDefault<z.ZodString>;
333
+ name: z.ZodDefault<z.ZodString>;
334
+ }, z.core.$strict>>;
335
+ }, z.core.$strict>>;
325
336
  }, z.core.$strict>>;
326
337
  rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
327
338
  path: z.ZodString;
@@ -409,9 +420,10 @@ interface PipelineConfigValidationOptions {
409
420
  allowMissingLintFileReferences?: boolean;
410
421
  }
411
422
  declare function loadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig;
423
+ declare function loadPackagePipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig;
412
424
  declare function tryLoadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig | null;
413
425
  declare function parsePipelineConfigYaml(source: string, sourcePath?: string, projectRoot?: string): PipelineConfig;
414
426
  declare function parsePipelineConfigParts(sources: PipelineConfigParts, projectRoot?: string, sourcePaths?: PipelineConfigParts, options?: PipelineConfigValidationOptions): PipelineConfig;
415
427
  declare function validatePipelineConfig(rawConfig: PipelineConfig, projectRoot?: string, options?: PipelineConfigValidationOptions): PipelineConfig;
416
428
  //#endregion
417
- export { GateKind, HookEvent, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, ScheduleBaseline, SchedulingRole, WorkflowNodeKind, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
429
+ export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, GateKind, HookEvent, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, ScheduleBaseline, SchedulingRole, WorkflowNodeKind, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
package/dist/config.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import { resolveFileReference } from "./path-refs.js";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { existsSync } from "node:fs";
4
3
  import { parseDocument } from "yaml";
5
4
  import { z } from "zod";
6
5
  //#region src/config.ts
7
6
  const PIPELINE_CONFIG_PATH = ".pipeline/pipeline.yaml";
8
7
  const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
9
8
  const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
10
- const LEGACY_CONFIG_PATH = ".pipeline/config.toml";
11
9
  const ID_RE = /^[a-z][a-z0-9-]*$/;
12
10
  const RUNNER_TYPES = [
13
11
  "codex",
@@ -57,6 +55,10 @@ const RETRY_REASONS = [
57
55
  const SCHEDULE_BASELINES = ["epic", "pipe"];
58
56
  const SCHEDULING_ROLES = ["coverage", "implementation"];
59
57
  const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
58
+ const DEFAULT_RUNNER_JOB_GIT_COMMITTER = {
59
+ email: "git@oisin.ee",
60
+ name: "oisin-bot"
61
+ };
60
62
  const PACKAGE_DEFAULT_RUNNERS_YAML = `version: 1
61
63
  runners:
62
64
  codex:
@@ -73,7 +75,7 @@ runners:
73
75
  output_formats: [text, json, jsonl, json_schema]
74
76
  opencode:
75
77
  type: opencode
76
- model: default
78
+ model: openai/gpt-5.5
77
79
  capabilities:
78
80
  native_subagents: true
79
81
  rules: true
@@ -113,7 +115,8 @@ profiles:
113
115
  pipeline-researcher:
114
116
  runner: codex
115
117
  description: Research the requested task and produce structured findings.
116
- instructions: { inline: "Inspect first-party source, tests, docs, and task context before proposing changes." }
118
+ instructions: { inline: "Inspect first-party source, tests, docs, and task context for the current task only. Produce concise findings with file references and stop; do not perform open-ended repository exploration." }
119
+ timeout_ms: 900000
117
120
  mcp_servers: [pipeline-gateway]
118
121
  tools: [read, list, grep, glob, bash]
119
122
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
@@ -163,7 +166,7 @@ profiles:
163
166
  runner: codex
164
167
  scheduling_roles: [coverage]
165
168
  description: Audit the finished change against every acceptance criterion.
166
- instructions: { inline: "Audit the completed change against each canonical acceptance criterion independently." }
169
+ instructions: { inline: 'Audit the completed change against each canonical acceptance criterion independently. Return only valid JSON with top-level "verdict", "evidence", "acceptance", and optional "violations". Each "acceptance" entry must include "id", "verdict", and non-empty "evidence". Do not use Markdown fences or prose outside the JSON object.' }
167
170
  mcp_servers: [pipeline-gateway]
168
171
  tools: [read, list, grep, glob, bash]
169
172
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
@@ -181,7 +184,7 @@ profiles:
181
184
  runner: codex
182
185
  scheduling_roles: [coverage]
183
186
  description: Verify checks, implementation fit, and final evidence.
184
- instructions: { inline: "Verify checks, implementation fit, and final evidence." }
187
+ instructions: { inline: 'Verify checks, implementation fit, and final evidence. Return only valid JSON with top-level "verdict", "evidence", and optional "violations". Do not use Markdown fences or prose outside the JSON object.' }
185
188
  mcp_servers: [pipeline-gateway]
186
189
  tools: [read, list, grep, glob, bash]
187
190
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
@@ -213,7 +216,7 @@ hooks:
213
216
  functions:
214
217
  generated-defaults-audit:
215
218
  kind: command
216
- command: [node, -e, "process.exit(0)"]
219
+ command: [node, -e, "const fs=require('node:fs'); fs.writeFileSync(process.env.PIPELINE_HOOK_RESULT, JSON.stringify({status:'pass',summary:'Generated defaults audit passed'}));"]
217
220
  trusted: true
218
221
  timeout_ms: 5000
219
222
  output_limit_bytes: 4096
@@ -340,6 +343,10 @@ workflows:
340
343
  kind: agent
341
344
  profile: pipeline-thermo-nuclear-reviewer
342
345
  needs: [merge]
346
+ gates:
347
+ - id: review-verdict
348
+ kind: verdict
349
+ target: stdout
343
350
  `;
344
351
  var PipelineConfigError = class extends Error {
345
352
  code;
@@ -515,6 +522,7 @@ const profileSchema = z.object({
515
522
  runner: z.string(),
516
523
  scheduling_roles: z.array(z.enum(SCHEDULING_ROLES)).optional(),
517
524
  skills: z.array(z.string()).optional(),
525
+ timeout_ms: z.number().int().positive().optional(),
518
526
  tools: z.array(z.enum(TOOL_NAMES)).optional()
519
527
  }).strict();
520
528
  const orchestratorSchema = z.object({ profile: z.string() }).strict();
@@ -654,10 +662,18 @@ const runnerJobEnvironmentSchema = z.object({
654
662
  setup: z.array(runnerJobCommandSchema).default([]),
655
663
  smoke: z.array(runnerJobCommandSchema).default([])
656
664
  }).strict();
657
- const runnerJobConfigSchema = z.object({ environment: runnerJobEnvironmentSchema.default({
658
- setup: [],
659
- smoke: []
660
- }) }).strict();
665
+ const runnerJobGitCommitterSchema = z.object({
666
+ email: z.string().email().default(DEFAULT_RUNNER_JOB_GIT_COMMITTER.email),
667
+ name: z.string().min(1).default(DEFAULT_RUNNER_JOB_GIT_COMMITTER.name)
668
+ }).strict();
669
+ const runnerJobGitSchema = z.object({ committer: runnerJobGitCommitterSchema.default(DEFAULT_RUNNER_JOB_GIT_COMMITTER) }).strict();
670
+ const runnerJobConfigSchema = z.object({
671
+ environment: runnerJobEnvironmentSchema.default({
672
+ setup: [],
673
+ smoke: []
674
+ }),
675
+ git: runnerJobGitSchema.default({ committer: DEFAULT_RUNNER_JOB_GIT_COMMITTER })
676
+ }).strict();
661
677
  const runnersFileSchema = z.object({
662
678
  runners: strictRecord(runnerSchema).default({}),
663
679
  version: z.literal(1)
@@ -678,10 +694,13 @@ const pipelineFileSchema = z.object({
678
694
  on: {}
679
695
  }),
680
696
  orchestrator: orchestratorSchema,
681
- runner_job: runnerJobConfigSchema.default({ environment: {
682
- setup: [],
683
- smoke: []
684
- } }),
697
+ runner_job: runnerJobConfigSchema.default({
698
+ environment: {
699
+ setup: [],
700
+ smoke: []
701
+ },
702
+ git: { committer: DEFAULT_RUNNER_JOB_GIT_COMMITTER }
703
+ }),
685
704
  schedules: strictRecord(schedulePolicySchema).default({}),
686
705
  task_context: taskContextResolverSchema.optional(),
687
706
  workflows: strictRecord(workflowSchema).default({}),
@@ -698,10 +717,13 @@ const configSchema = z.object({
698
717
  mcp_servers: strictRecord(mcpServerSchema).default({}),
699
718
  orchestrator: orchestratorSchema,
700
719
  profiles: strictRecord(profileSchema).default({}),
701
- runner_job: runnerJobConfigSchema.default({ environment: {
702
- setup: [],
703
- smoke: []
704
- } }),
720
+ runner_job: runnerJobConfigSchema.default({
721
+ environment: {
722
+ setup: [],
723
+ smoke: []
724
+ },
725
+ git: { committer: DEFAULT_RUNNER_JOB_GIT_COMMITTER }
726
+ }),
705
727
  rules: strictRecord(pathRefSchema).default({}),
706
728
  runners: strictRecord(runnerSchema).default({}),
707
729
  schedules: strictRecord(schedulePolicySchema).default({}),
@@ -769,36 +791,18 @@ function addConfigSchemaIssue(ctx, path, message) {
769
791
  });
770
792
  }
771
793
  function loadPipelineConfig(projectRoot, options = {}) {
772
- const paths = [
773
- PIPELINE_CONFIG_PATH,
774
- PROFILES_CONFIG_PATH,
775
- RUNNERS_CONFIG_PATH
776
- ];
777
- const missing = paths.filter((path) => !existsSync(join(projectRoot, path)));
778
- if (missing.length > 0) {
779
- if (existsSync(join(projectRoot, LEGACY_CONFIG_PATH))) throw new PipelineConfigError("PIPELINE_CONFIG_LEGACY_UNSUPPORTED", `${LEGACY_CONFIG_PATH} is not supported by the v1 pipeline config. Create ${PIPELINE_CONFIG_PATH}.`, [{
780
- path: LEGACY_CONFIG_PATH,
781
- message: "legacy TOML config found"
782
- }]);
783
- if (missing.length === paths.length) return parsePipelineConfigParts({
784
- pipeline: PACKAGE_DEFAULT_PIPELINE_YAML,
785
- profiles: PACKAGE_DEFAULT_PROFILES_YAML,
786
- runners: PACKAGE_DEFAULT_RUNNERS_YAML
787
- }, projectRoot, {
788
- pipeline: "@oisincoveney/pipeline/defaults/pipeline.yaml",
789
- profiles: "@oisincoveney/pipeline/defaults/profiles.yaml",
790
- runners: "@oisincoveney/pipeline/defaults/runners.yaml"
791
- }, options);
792
- throw new PipelineConfigError("PIPELINE_CONFIG_MISSING", `Missing required pipeline config files: ${missing.join(", ")}`, missing.map((path) => ({
793
- path,
794
- message: "file does not exist"
795
- })));
796
- }
794
+ return loadPackagePipelineConfig(projectRoot, options);
795
+ }
796
+ function loadPackagePipelineConfig(projectRoot, options = {}) {
797
797
  return parsePipelineConfigParts({
798
- pipeline: readFileSync(join(projectRoot, PIPELINE_CONFIG_PATH), "utf8"),
799
- profiles: readFileSync(join(projectRoot, PROFILES_CONFIG_PATH), "utf8"),
800
- runners: readFileSync(join(projectRoot, RUNNERS_CONFIG_PATH), "utf8")
801
- }, projectRoot, void 0, options);
798
+ pipeline: PACKAGE_DEFAULT_PIPELINE_YAML,
799
+ profiles: PACKAGE_DEFAULT_PROFILES_YAML,
800
+ runners: PACKAGE_DEFAULT_RUNNERS_YAML
801
+ }, projectRoot, {
802
+ pipeline: "@oisincoveney/pipeline/defaults/pipeline.yaml",
803
+ profiles: "@oisincoveney/pipeline/defaults/profiles.yaml",
804
+ runners: "@oisincoveney/pipeline/defaults/runners.yaml"
805
+ }, options);
802
806
  }
803
807
  function tryLoadPipelineConfig(projectRoot, options = {}) {
804
808
  return loadPipelineConfig(projectRoot, options);
@@ -1066,4 +1070,4 @@ function validationError(issues) {
1066
1070
  return new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", ["Invalid pipeline config:", ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
1067
1071
  }
1068
1072
  //#endregion
1069
- export { PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
1073
+ export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
package/dist/index.d.ts CHANGED
@@ -9,8 +9,8 @@ interface PipeOptions {
9
9
  workflow?: string;
10
10
  }
11
11
  /**
12
- * Config-driven `pipe` entrypoint. The workflow source of truth is
13
- * `.pipeline/pipeline.yaml`; missing YAML is a hard error.
12
+ * Config-driven `pipe` entrypoint. Package-owned defaults are the source of
13
+ * truth; repo-local pipeline files are only scaffolding fixtures.
14
14
  */
15
15
  declare function pipe(description: string, options?: PipeOptions): Promise<void>;
16
16
  interface DoctorCheck {
package/dist/index.js CHANGED
@@ -18,8 +18,8 @@ import { execa } from "execa";
18
18
  const PATH_SEPARATOR_RE = /[\\/]/;
19
19
  const LINE_RE = /\r?\n/;
20
20
  /**
21
- * Config-driven `pipe` entrypoint. The workflow source of truth is
22
- * `.pipeline/pipeline.yaml`; missing YAML is a hard error.
21
+ * Config-driven `pipe` entrypoint. Package-owned defaults are the source of
22
+ * truth; repo-local pipeline files are only scaffolding fixtures.
23
23
  */
24
24
  function pipe(description, options = {}) {
25
25
  try {
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
2
  import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
4
  import { execa } from "execa";
5
5
  //#region src/mcp/gateway.ts
6
6
  const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
package/dist/path-refs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { isAbsolute, join } from "node:path";
2
1
  import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
3
  //#region src/path-refs.ts
4
4
  function resolveFileReference(basePath, pathRef) {
5
5
  if (pathRef === "~") return homedir();
@@ -19,7 +19,7 @@ import { dispatchHooks } from "./runtime/hooks/hooks.js";
19
19
  import "./runtime/hooks/index.js";
20
20
  import { executeParallelNode } from "./runtime/parallel-node/parallel-node.js";
21
21
  import "./runtime/parallel-node/index.js";
22
- import { prepareWorkflowNodeWorktree, removeWorkflowNodeWorktree, workflowBaseSha } from "./runtime/worktrees/worktrees.js";
22
+ import { commitWorkflowNodeWorktree, prepareWorkflowNodeWorktree, removeWorkflowNodeWorktree, workflowBaseSha } from "./runtime/worktrees/worktrees.js";
23
23
  import "./runtime/worktrees/index.js";
24
24
  import { nodeExecutionMachine } from "./runtime-machines/node-machine.js";
25
25
  import { workflowSchedulerMachine } from "./runtime-machines/workflow-machine.js";
@@ -572,10 +572,36 @@ async function executeWorkflowNode(node, context) {
572
572
  childContext.inheritedOutputNodeIds = new Set(childContext.lastOutputByNode.keys());
573
573
  const result = await runPipelineWithContext(childContext);
574
574
  context.agentInvocations.push(...result.agentInvocations);
575
+ if (result.outcome === "PASS" && worktree.worktreePath) try {
576
+ worktree.commitSha = await commitWorkflowNodeWorktree(worktree.worktreePath, node.id, context.config.runner_job.git.committer);
577
+ } catch (error) {
578
+ const message = error instanceof Error ? error.message : String(error);
579
+ return {
580
+ evidence: [
581
+ `workflow '${result.plan.workflowId}' passed`,
582
+ `workflow worktree commit failed: ${message}`,
583
+ `inspect workflow worktree: cd ${worktree.worktreePath}`
584
+ ],
585
+ exitCode: 1,
586
+ output: JSON.stringify({
587
+ baseSha: worktree.baseSha,
588
+ branch: worktree.branch,
589
+ commitSha: worktree.commitSha ?? null,
590
+ nodeResults: result.nodes.map((child) => ({
591
+ nodeId: child.nodeId,
592
+ status: child.status
593
+ })),
594
+ status: "FAIL",
595
+ worktreePath: worktree.worktreePath,
596
+ workflowId: result.plan.workflowId
597
+ })
598
+ };
599
+ }
575
600
  if (result.outcome === "PASS" && worktree.worktreePath && !shouldPreserveWorkflowNodeWorktree(node, context)) await removeWorkflowNodeWorktree(worktree.worktreePath);
576
601
  const output = JSON.stringify({
577
602
  baseSha: worktree.baseSha,
578
603
  branch: worktree.branch,
604
+ ...worktree.worktreePath ? { commitSha: worktree.commitSha ?? null } : {},
579
605
  nodeResults: result.nodes.map((child) => ({
580
606
  nodeId: child.nodeId,
581
607
  status: child.status
@@ -1,30 +1,36 @@
1
+ import { DEFAULT_RUNNER_JOB_GIT_COMMITTER } from "../config.js";
1
2
  import { execa } from "execa";
3
+ import simpleGit$1 from "simple-git";
2
4
  //#region src/runner-job/delivery.ts
3
5
  const GITHUB_HTTPS_REPOSITORY_RE = /^https:\/\/github\.com\/([^/]+\/[^/.]+)(?:\.git)?$/;
4
6
  const GITHUB_SSH_REPOSITORY_RE = /^git@github\.com:([^/]+\/[^/.]+)(?:\.git)?$/;
7
+ const deliverGitBranch = async (options) => {
8
+ const git = createDeliveryGitClient(options);
9
+ const branch = options.branch ?? await currentBranch(git);
10
+ const status = await git.status();
11
+ let commitSha = null;
12
+ if (status.files.length > 0) {
13
+ await git.add(["--all"]);
14
+ await configureCommitter(git, options.committer ?? DEFAULT_RUNNER_JOB_GIT_COMMITTER);
15
+ await git.commit(deliveryCommitMessage(options.payload));
16
+ if ((await git.status()).files.length > 0) throw new Error("Runner job delivery requires all worktree changes to be committed");
17
+ commitSha = (await git.revparse(["HEAD"])).trim();
18
+ }
19
+ for (const branchToPush of await deliveryBranches(git, branch, options.payload.run.id)) await git.push("origin", branchToPush, ["--set-upstream"]);
20
+ return {
21
+ branch,
22
+ commitSha,
23
+ pushed: true
24
+ };
25
+ };
5
26
  const createPullRequest = async (options) => {
6
27
  if (!options.payload.delivery.pullRequest) return null;
7
28
  const repository = options.payload.repository;
8
29
  const env = compactEnv(options.env);
9
30
  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");
31
+ const branch = options.branch ?? await currentBranch(createDeliveryGitClient(options));
16
32
  const headOwner = options.env.PIPELINE_PR_HEAD_OWNER?.trim() || "oisin-bot";
17
33
  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
34
  const url = (await runCommand("gh", [
29
35
  "pr",
30
36
  "create",
@@ -43,6 +49,28 @@ const createPullRequest = async (options) => {
43
49
  return url ? { url } : null;
44
50
  };
45
51
  const runDeliveryCommand = (command, args, options) => execa(command, args, options);
52
+ function createDeliveryGitClient(options) {
53
+ return options.createGitClient?.(options.worktreePath) ?? simpleGit$1({ baseDir: options.worktreePath });
54
+ }
55
+ async function currentBranch(git) {
56
+ const branch = (await git.branch()).current.trim();
57
+ if (!branch) throw new Error("Runner job delivery requires a checked-out branch");
58
+ return branch;
59
+ }
60
+ async function deliveryBranches(git, current, runId) {
61
+ const branches = await git.branchLocal();
62
+ return [...new Set([current, ...Object.keys(branches.branches).map((branch) => branch.trim()).filter((branch) => isRunScopedBranch(branch, runId))])];
63
+ }
64
+ function isRunScopedBranch(branch, runId) {
65
+ return branch.startsWith(`${runId}/`) || branch === `runs/integration/${runId}`;
66
+ }
67
+ function deliveryCommitMessage(payload) {
68
+ return `pipeline: ${(payload.task.kind === "ticket" ? payload.task.id.trim() : void 0) || payload.run.id}`;
69
+ }
70
+ async function configureCommitter(git, committer) {
71
+ await git.addConfig("user.name", committer.name, false, "local");
72
+ await git.addConfig("user.email", committer.email, false, "local");
73
+ }
46
74
  function compactEnv(env) {
47
75
  return Object.fromEntries(Object.entries(env).filter((entry) => Boolean(entry[1])));
48
76
  }
@@ -54,4 +82,4 @@ function githubRepositoryName(repositoryUrl) {
54
82
  throw new Error("Pull request delivery requires a GitHub repository URL");
55
83
  }
56
84
  //#endregion
57
- export { createPullRequest };
85
+ export { createPullRequest, deliverGitBranch };