@oisincoveney/pipeline 3.24.2 → 4.0.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.
Files changed (382) hide show
  1. package/README.md +14 -2
  2. package/defaults/pipeline.yaml +23 -5
  3. package/defaults/profiles.yaml +104 -21
  4. package/dist/argo-graph.js +90 -88
  5. package/dist/argo-submit.d.ts +110 -138
  6. package/dist/argo-submit.js +363 -378
  7. package/dist/argo-workflow.d.ts +156 -177
  8. package/dist/argo-workflow.js +96 -138
  9. package/dist/backlog.js +5 -4
  10. package/dist/bench/eval-report.js +21 -12
  11. package/dist/claude-settings-config.js +23 -31
  12. package/dist/claude-user-config.js +6 -16
  13. package/dist/cli/bootstrap-commands.js +3 -3
  14. package/dist/cli/doctor.d.ts +1 -1
  15. package/dist/cli/doctor.js +176 -171
  16. package/dist/cli/factory-commands.js +13 -13
  17. package/dist/cli/format.js +92 -86
  18. package/dist/cli/loop-commands.js +21 -21
  19. package/dist/cli/mcp-gateway-commands.js +12 -12
  20. package/dist/cli/plan-commands.js +85 -116
  21. package/dist/cli/program.d.ts +3 -3
  22. package/dist/cli/program.js +41 -43
  23. package/dist/cli/run-command.d.ts +1 -1
  24. package/dist/cli/run-command.js +25 -25
  25. package/dist/cli/run-commands.js +31 -29
  26. package/dist/cli/run-resolver.d.ts +1 -1
  27. package/dist/cli/run-resolver.js +30 -32
  28. package/dist/cli/run-service.d.ts +2 -3
  29. package/dist/cli/run-service.js +229 -239
  30. package/dist/cli/submit-options.js +85 -83
  31. package/dist/cluster-doctor.js +147 -175
  32. package/dist/codex-config.js +24 -30
  33. package/dist/commands/bench-command.js +9 -7
  34. package/dist/commands/coordinator-command.js +35 -0
  35. package/dist/commands/pipeline-command.js +13 -7
  36. package/dist/commands/runner-command-command.js +39 -24
  37. package/dist/commands/runner-node-command.js +31 -0
  38. package/dist/commands/ticket/complete.js +29 -33
  39. package/dist/commands/ticket/create.js +84 -82
  40. package/dist/commands/ticket/graph-check.js +11 -11
  41. package/dist/commands/ticket/next.js +36 -36
  42. package/dist/commands/ticket/sequence.js +11 -12
  43. package/dist/commands/ticket/shared.d.ts +5 -2
  44. package/dist/commands/ticket/shared.js +63 -66
  45. package/dist/commands/ticket/start.js +58 -67
  46. package/dist/commands/ticket-command.js +2 -2
  47. package/dist/config/defaults.d.ts +139 -140
  48. package/dist/config/defaults.js +81 -80
  49. package/dist/config/lint.js +52 -64
  50. package/dist/config/load.d.ts +4 -5
  51. package/dist/config/load.js +64 -74
  52. package/dist/config/schema/catalog.d.ts +2 -1
  53. package/dist/config/schema/catalog.js +3 -2
  54. package/dist/config/schema/mcp.js +46 -52
  55. package/dist/config/schemas.d.ts +382 -528
  56. package/dist/config/schemas.js +400 -398
  57. package/dist/config/validate.d.ts +1 -2
  58. package/dist/config/validate.js +236 -201
  59. package/dist/config.d.ts +2 -2
  60. package/dist/context/repo-map.js +105 -119
  61. package/dist/coordinator/composition.js +169 -0
  62. package/dist/coordinator/config.js +71 -0
  63. package/dist/coordinator/run-owner.js +64 -0
  64. package/dist/coordinator/wave-advance.js +148 -0
  65. package/dist/coordinator/wave-scheduler.js +50 -0
  66. package/dist/credentials/broker.d.ts +6 -8
  67. package/dist/credentials/broker.js +17 -16
  68. package/dist/credentials/codex-config.js +15 -19
  69. package/dist/credentials/file-targets.js +9 -15
  70. package/dist/credentials/local-codex-auth-sync.js +55 -58
  71. package/dist/credentials/opencode-config.js +28 -32
  72. package/dist/credentials/runner.js +13 -13
  73. package/dist/effect-sync-errors.js +28 -0
  74. package/dist/factory/create-experiment.js +285 -121
  75. package/dist/factory/exec.js +10 -12
  76. package/dist/factory/git-credentials.js +4 -4
  77. package/dist/factory/stamp-answers.js +10 -11
  78. package/dist/factory/template-update.js +65 -44
  79. package/dist/factory-lane.d.ts +26 -40
  80. package/dist/factory-lane.js +26 -25
  81. package/dist/gates.js +3 -3
  82. package/dist/git-remote-url.js +17 -19
  83. package/dist/hooks.d.ts +26 -26
  84. package/dist/hooks.js +20 -22
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.js +27 -36
  87. package/dist/install-commands/claude-code.js +100 -92
  88. package/dist/install-commands/host-selection.js +3 -6
  89. package/dist/install-commands/opencode.js +264 -259
  90. package/dist/install-commands/planner.js +163 -181
  91. package/dist/install-commands/result-format.js +1 -3
  92. package/dist/install-commands/shared.js +25 -42
  93. package/dist/install-commands/writer.js +22 -25
  94. package/dist/install-commands.js +8 -8
  95. package/dist/jscpd-output.js +29 -0
  96. package/dist/json-config-merge.js +14 -20
  97. package/dist/json-line-values.js +17 -6
  98. package/dist/loop/argo-poll.d.ts +0 -1
  99. package/dist/loop/argo-poll.js +15 -95
  100. package/dist/loop/backlog-records.js +1 -3
  101. package/dist/loop/controller-deps.js +150 -161
  102. package/dist/loop/controller.js +164 -186
  103. package/dist/loop/gh-checks.js +62 -61
  104. package/dist/loop/gh-runner.js +22 -25
  105. package/dist/loop/loop-command.js +66 -58
  106. package/dist/loop/loop-controller-entrypoint.js +29 -30
  107. package/dist/loop/merge.js +36 -40
  108. package/dist/loop/run-terminal-poll.js +69 -0
  109. package/dist/mcp/gateway-config.js +25 -29
  110. package/dist/mcp/gateway-doctor.js +77 -119
  111. package/dist/mcp/gateway-reconcile.js +17 -22
  112. package/dist/mcp/gateway-runtime.js +1 -3
  113. package/dist/mcp/host-config.js +37 -40
  114. package/dist/mcp/host-renderers.js +10 -18
  115. package/dist/mcp/repo-local-backends.js +51 -54
  116. package/dist/mcp/toolhive-vmcp.js +39 -39
  117. package/dist/model-resolver.js +29 -33
  118. package/dist/moka-global-config.d.ts +78 -55
  119. package/dist/moka-global-config.js +93 -92
  120. package/dist/moka-submit.d.ts +2179 -866
  121. package/dist/moka-submit.js +97 -99
  122. package/dist/opencode-project-config.js +34 -40
  123. package/dist/package-assets.js +1 -3
  124. package/dist/path-refs.js +2 -2
  125. package/dist/pipeline-init.js +25 -25
  126. package/dist/pipeline-runtime.d.ts +15 -14
  127. package/dist/pipeline-runtime.js +51 -50
  128. package/dist/planned-node.js +6 -3
  129. package/dist/planning/compile.d.ts +5 -5
  130. package/dist/planning/compile.js +140 -139
  131. package/dist/planning/dependency-refs.js +5 -5
  132. package/dist/planning/generate.d.ts +1296 -1296
  133. package/dist/planning/generate.js +307 -314
  134. package/dist/planning/graph.d.ts +0 -1
  135. package/dist/planning/graph.js +131 -149
  136. package/dist/remote/argo/model.d.ts +104 -122
  137. package/dist/remote/argo/model.js +205 -197
  138. package/dist/remote/argo/policy.js +39 -47
  139. package/dist/remote/argo/storage.js +81 -85
  140. package/dist/remote/jobs/execution-identity.js +70 -0
  141. package/dist/remote/jobs/job-outcome.js +108 -0
  142. package/dist/remote/jobs/manifest.js +96 -0
  143. package/dist/remote/jobs/model.d.ts +158 -0
  144. package/dist/remote/jobs/model.js +162 -0
  145. package/dist/remote/jobs/node-bootstrap.js +213 -0
  146. package/dist/remote/jobs/policy.js +123 -0
  147. package/dist/remote/jobs/reconcile.js +100 -0
  148. package/dist/remote/jobs/run-spec-client.js +64 -0
  149. package/dist/remote/jobs/run-spec-path.js +31 -0
  150. package/dist/remote/jobs/run-spec-server.d.ts +34 -0
  151. package/dist/remote/jobs/run-spec-server.js +116 -0
  152. package/dist/remote/jobs/submit.d.ts +23 -0
  153. package/dist/remote/jobs/submit.js +116 -0
  154. package/dist/remote/jobs/watch.js +59 -0
  155. package/dist/remote/submit/argo-submission.d.ts +3 -1
  156. package/dist/remote/submit/argo-submission.js +53 -58
  157. package/dist/remote/submit/compilation.d.ts +2 -0
  158. package/dist/remote/submit/compilation.js +46 -49
  159. package/dist/remote/submit/event-boundary.js +62 -76
  160. package/dist/remote/submit/hook-events.d.ts +6 -0
  161. package/dist/remote/submit/io.d.ts +7 -1
  162. package/dist/remote/submit/io.js +40 -43
  163. package/dist/remote/submit/jobs-submission.d.ts +59 -0
  164. package/dist/remote/submit/jobs-submission.js +97 -0
  165. package/dist/remote/submit/jobs-submit-result.d.ts +25 -0
  166. package/dist/remote/submit/jobs-submit-result.js +34 -0
  167. package/dist/remote/submit/options.d.ts +49 -0
  168. package/dist/remote/submit/options.js +27 -0
  169. package/dist/remote/submit/service.d.ts +40 -1
  170. package/dist/remote/submit/service.js +67 -49
  171. package/dist/run-control/command-context.js +6 -8
  172. package/dist/run-control/commands.js +10 -10
  173. package/dist/run-control/contracts.d.ts +52 -101
  174. package/dist/run-control/contracts.js +83 -69
  175. package/dist/run-control/detach.js +66 -72
  176. package/dist/run-control/execution-store.js +0 -0
  177. package/dist/run-control/file-errors.js +1 -3
  178. package/dist/run-control/logical-segment.js +3 -3
  179. package/dist/run-control/next-node.js +67 -78
  180. package/dist/run-control/postgres/postgres-run-control-store.js +196 -225
  181. package/dist/run-control/resume-command.js +34 -41
  182. package/dist/run-control/run-artifacts-command.js +84 -105
  183. package/dist/run-control/run-command-domain.js +7 -11
  184. package/dist/run-control/run-control-store.js +9 -9
  185. package/dist/run-control/run-query-command.js +73 -100
  186. package/dist/run-control/run-record.js +5 -3
  187. package/dist/run-control/run-state-lock.js +5 -5
  188. package/dist/run-control/runtime-event-projection.js +38 -50
  189. package/dist/run-control/runtime-reporter.js +86 -102
  190. package/dist/run-control/stop-command.js +42 -53
  191. package/dist/run-control/store-fs-effects.js +3 -1
  192. package/dist/run-control/store-manifest.js +38 -13
  193. package/dist/run-control/store-paths.js +3 -3
  194. package/dist/run-control/store.js +1 -1
  195. package/dist/run-control/submit-result.js +21 -21
  196. package/dist/run-control/supervisor.js +78 -68
  197. package/dist/run-control/workspace.js +2 -2
  198. package/dist/run-state/git-refs.js +258 -307
  199. package/dist/runner/opencode-excludes.js +4 -4
  200. package/dist/runner/subprocess-result.js +28 -37
  201. package/dist/runner/subprocess.js +41 -41
  202. package/dist/runner/timeouts.js +14 -13
  203. package/dist/runner-command/dynamic-command.js +46 -39
  204. package/dist/runner-command/environment-commands.js +91 -0
  205. package/dist/runner-command/finalize.js +174 -153
  206. package/dist/runner-command/lifecycle-context.js +115 -130
  207. package/dist/runner-command/lifecycle.js +78 -72
  208. package/dist/runner-command/pre-schedule.js +203 -223
  209. package/dist/runner-command/run.js +450 -448
  210. package/dist/runner-command/schedule-source-options.js +8 -10
  211. package/dist/runner-command/select-ready-wave.js +39 -30
  212. package/dist/runner-command/task-descriptor.js +11 -17
  213. package/dist/runner-command-contract.d.ts +147 -157
  214. package/dist/runner-command-contract.js +403 -411
  215. package/dist/runner-event-schema.d.ts +364 -505
  216. package/dist/runner-event-schema.js +187 -175
  217. package/dist/runner-event-sink.js +37 -43
  218. package/dist/runner-output.js +6 -6
  219. package/dist/runner.d.ts +57 -37
  220. package/dist/runner.js +176 -127
  221. package/dist/runtime/actor-ids.d.ts +1 -0
  222. package/dist/runtime/actor-ids.js +2 -2
  223. package/dist/runtime/agent-node/agent-node.js +99 -110
  224. package/dist/runtime/agent-node/handoff-finalization.js +23 -24
  225. package/dist/runtime/agent-node/model-selection.js +19 -22
  226. package/dist/runtime/agent-node/output-finalization.js +174 -204
  227. package/dist/runtime/agent-node/prompt-rendering.js +208 -251
  228. package/dist/runtime/agent-node/session-execution.js +43 -39
  229. package/dist/runtime/builtins/builtins.js +218 -284
  230. package/dist/runtime/changed-files/changed-files.js +29 -34
  231. package/dist/runtime/command-executor/command-executor.js +31 -23
  232. package/dist/runtime/config-error.js +1 -3
  233. package/dist/runtime/context/context.js +35 -37
  234. package/dist/runtime/contracts/contracts.d.ts +3 -0
  235. package/dist/runtime/contracts/index.d.ts +1 -1
  236. package/dist/runtime/detached-race.js +11 -14
  237. package/dist/runtime/drain-merge/drain-merge.js +139 -164
  238. package/dist/runtime/durable-store/acquisition.js +5 -3
  239. package/dist/runtime/durable-store/durable-store.d.ts +3 -1
  240. package/dist/runtime/durable-store/postgres/client.js +29 -0
  241. package/dist/runtime/durable-store/postgres/migrate-substrate.js +34 -29
  242. package/dist/runtime/durable-store/postgres/postgres-store.js +18 -20
  243. package/dist/runtime/events/events.js +273 -331
  244. package/dist/runtime/events/index.js +2 -2
  245. package/dist/runtime/execution-envelope.js +44 -0
  246. package/dist/runtime/gates/adjudication/llm-judge.js +21 -24
  247. package/dist/runtime/gates/adjudication/structured-claim.js +15 -16
  248. package/dist/runtime/gates/adjudicator/adjudicator.js +27 -27
  249. package/dist/runtime/gates/adjudicator/index.js +2 -2
  250. package/dist/runtime/gates/contract.js +5 -9
  251. package/dist/runtime/gates/gates.js +9 -8
  252. package/dist/runtime/gates/kinds/acceptance/acceptance.js +68 -71
  253. package/dist/runtime/gates/kinds/acceptance/index.js +2 -2
  254. package/dist/runtime/gates/kinds/artifact/artifact.js +4 -4
  255. package/dist/runtime/gates/kinds/artifact/index.js +2 -2
  256. package/dist/runtime/gates/kinds/builtin/builtin.js +4 -4
  257. package/dist/runtime/gates/kinds/builtin/index.js +2 -2
  258. package/dist/runtime/gates/kinds/changed-files/changed-files.js +31 -35
  259. package/dist/runtime/gates/kinds/changed-files/index.js +2 -2
  260. package/dist/runtime/gates/kinds/command/command.js +3 -3
  261. package/dist/runtime/gates/kinds/command/index.js +2 -2
  262. package/dist/runtime/gates/kinds/json-schema/index.js +2 -2
  263. package/dist/runtime/gates/kinds/json-schema/json-schema.js +7 -6
  264. package/dist/runtime/gates/kinds/verdict/index.js +2 -2
  265. package/dist/runtime/gates/kinds/verdict/verdict.js +3 -3
  266. package/dist/runtime/gates/orchestrator.js +72 -86
  267. package/dist/runtime/gates/registry.js +1 -3
  268. package/dist/runtime/handoff.d.ts +2 -1
  269. package/dist/runtime/handoff.js +43 -46
  270. package/dist/runtime/hooks/command-hook.js +57 -71
  271. package/dist/runtime/hooks/context.js +21 -25
  272. package/dist/runtime/hooks/events.js +84 -100
  273. package/dist/runtime/hooks/execution.js +6 -12
  274. package/dist/runtime/hooks/hooks.js +34 -23
  275. package/dist/runtime/hooks/invocation.js +23 -29
  276. package/dist/runtime/hooks/module-hook.js +36 -35
  277. package/dist/runtime/hooks/policy.js +32 -29
  278. package/dist/runtime/hooks/results.js +56 -66
  279. package/dist/runtime/journal-acquisition.d.ts +3 -2
  280. package/dist/runtime/journal-acquisition.js +14 -14
  281. package/dist/runtime/json-validation/json-validation.js +51 -72
  282. package/dist/runtime/launch-plan-args.js +39 -0
  283. package/dist/runtime/local-scheduler.js +46 -48
  284. package/dist/runtime/node-execution.js +376 -445
  285. package/dist/runtime/node-protocol/node-protocol.js +36 -79
  286. package/dist/runtime/node-state-store.d.ts +4 -1
  287. package/dist/runtime/node-state-store.js +13 -57
  288. package/dist/runtime/node-state-tracker.js +46 -54
  289. package/dist/runtime/open-pull-request/open-pull-request.js +201 -187
  290. package/dist/runtime/opencode-adapter.js +23 -24
  291. package/dist/runtime/opencode-agent-name.js +6 -6
  292. package/dist/runtime/opencode-runtime.js +115 -93
  293. package/dist/runtime/opencode-server.js +39 -53
  294. package/dist/runtime/opencode-session-executor.js +312 -344
  295. package/dist/runtime/parallel-node/parallel-node.js +123 -134
  296. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +90 -73
  297. package/dist/runtime/protected-paths/protected-paths.js +70 -71
  298. package/dist/runtime/remediation/remediation.js +202 -231
  299. package/dist/runtime/retry.js +8 -10
  300. package/dist/runtime/run-journal.d.ts +2 -0
  301. package/dist/runtime/run-journal.js +7 -7
  302. package/dist/runtime/runtime-results.js +54 -69
  303. package/dist/runtime/scheduled-dependencies.d.ts +2 -1
  304. package/dist/runtime/scheduled-dependencies.js +61 -75
  305. package/dist/runtime/scheduler.js +179 -202
  306. package/dist/runtime/services/agent-node-runtime-service.js +3 -3
  307. package/dist/runtime/services/backlog-service.d.ts +1 -2
  308. package/dist/runtime/services/backlog-service.js +8 -14
  309. package/dist/runtime/services/command-executor-service.js +1 -1
  310. package/dist/runtime/services/config-io-service.js +24 -21
  311. package/dist/runtime/services/drain-merge-git-service.js +1 -1
  312. package/dist/runtime/services/file-system-service.js +9 -7
  313. package/dist/runtime/services/git-porcelain-service.js +6 -6
  314. package/dist/runtime/services/kubernetes-argo-service.d.ts +10 -3
  315. package/dist/runtime/services/kubernetes-argo-service.js +90 -84
  316. package/dist/runtime/services/kubernetes-jobs-read-service.js +115 -0
  317. package/dist/runtime/services/kubernetes-jobs-service.d.ts +37 -0
  318. package/dist/runtime/services/kubernetes-jobs-service.js +45 -0
  319. package/dist/runtime/services/kubernetes-jobs-watch-service.js +61 -0
  320. package/dist/runtime/services/kubernetes-lease-service.js +21 -0
  321. package/dist/runtime/services/mcp-gateway-service.js +143 -94
  322. package/dist/runtime/services/open-pull-request-git-service.js +1 -3
  323. package/dist/runtime/services/opencode-runtime-server-service.js +10 -12
  324. package/dist/runtime/services/opencode-sdk-service.js +7 -7
  325. package/dist/runtime/services/repo-io-service.js +10 -12
  326. package/dist/runtime/services/run-journal-file-service.js +2 -2
  327. package/dist/runtime/services/runner-command-io-service.js +48 -52
  328. package/dist/runtime/services/runner-event-sink-http-service.js +32 -39
  329. package/dist/runtime/step/step-node.js +15 -15
  330. package/dist/runtime/workflow-execution.js +56 -57
  331. package/dist/runtime/workflow-lifecycle.js +82 -101
  332. package/dist/runtime/yeet-event-record.js +104 -0
  333. package/dist/runtime/yeet-executor.js +184 -0
  334. package/dist/safe-json.js +16 -16
  335. package/dist/schedule/backlog-context.js +122 -116
  336. package/dist/schedule/baseline.js +249 -255
  337. package/dist/schedule/passes/coverage.js +63 -68
  338. package/dist/schedule/passes/drain-merge.js +34 -40
  339. package/dist/schedule/passes/ids.js +15 -17
  340. package/dist/schedule/passes/models.js +40 -37
  341. package/dist/schedule/passes/open-pull-request.js +16 -20
  342. package/dist/schedule/passes/references.js +18 -22
  343. package/dist/schedule/prompts.js +135 -141
  344. package/dist/schedule/scheduling-roles.js +8 -14
  345. package/dist/schema-boundary.d.ts +4 -0
  346. package/dist/schema-boundary.js +145 -0
  347. package/dist/serialized-write-queue.js +2 -2
  348. package/dist/standard-output-schemas.js +61 -59
  349. package/dist/strings.js +12 -12
  350. package/dist/task-ref.js +5 -5
  351. package/dist/tickets/apply-ticket-plan.js +52 -61
  352. package/dist/tickets/backlog-task-store.d.ts +15 -23
  353. package/dist/tickets/backlog-task-store.js +82 -94
  354. package/dist/tickets/completion/complete-ticket.js +53 -59
  355. package/dist/tickets/ticket-graph-dto.d.ts +24 -48
  356. package/dist/tickets/ticket-graph-dto.js +40 -40
  357. package/dist/tickets/ticket-graph.d.ts +7 -2
  358. package/dist/tickets/ticket-graph.js +36 -50
  359. package/dist/tickets/ticket-plan-command-args.js +6 -10
  360. package/dist/tickets/ticket-plan-render.js +10 -16
  361. package/dist/tickets/ticket-plan.d.ts +32 -41
  362. package/dist/tickets/ticket-plan.js +97 -77
  363. package/dist/tickets/ticket-selection.d.ts +3 -1
  364. package/dist/tickets/ticket-selection.js +37 -52
  365. package/dist/tickets/ticket-task-index.js +13 -17
  366. package/dist/tickets/validation-error-format.js +3 -10
  367. package/dist/token-estimator.js +12 -7
  368. package/dist/unknown-fields.js +21 -0
  369. package/dist/workflow-submit-contract.d.ts +9 -10
  370. package/dist/workflow-submit-contract.js +10 -9
  371. package/docs/adr-moka-postgres-schema-isolation.md +48 -0
  372. package/docs/adr-opencode-first-goal-loop-runtime.md +11 -11
  373. package/docs/config-architecture.md +13 -13
  374. package/docs/mcp-host-isolation.md +1 -0
  375. package/docs/moka-orchestrator-design.md +3 -3
  376. package/docs/operator-guide.md +1 -1
  377. package/docs/pipeline-console-runner-contract.md +10 -6
  378. package/docs/runtime-actor-model.md +18 -18
  379. package/docs/runtime-test-subsystems.md +200 -0
  380. package/docs/slash-command-adapter-contract.md +3 -3
  381. package/package.json +49 -85
  382. package/dist/config/schema/reference-validation.js +0 -83
@@ -1,257 +1,163 @@
1
+ import { isStringValue } from "../schema-boundary.js";
2
+ import "../safe-json.js";
1
3
  import { GitPorcelainService, GitPorcelainServiceLive } from "../runtime/services/git-porcelain-service.js";
2
- import { Effect } from "effect";
3
4
  import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { dirname, resolve } from "node:path";
7
+ import { Effect, Option } from "effect";
6
8
  //#region src/run-state/git-refs.ts
7
9
  const DEFAULT_WORKSPACE_PATH = "/workspace";
8
10
  const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
9
11
  const WRITABLE_GIT_CREDENTIAL_STORE = resolve(tmpdir(), "pipeline-git-credentials");
10
12
  const LS_REMOTE_FIELD_SEPARATOR_RE = /\s+/u;
11
13
  const SCP_LIKE_SSH_REMOTE_RE = /^[^@\s]+@[^:\s]+:.+/u;
12
- let preparedBasicAuthCredentialStore;
13
- function runnerGitRefs(payload, nodeId) {
14
+ const MISSING_REMOTE_URL = Option.none();
15
+ let preparedBasicAuthCredentialStore = Option.none();
16
+ const runnerGitRefs = (payload, nodeId) => {
14
17
  const prefix = `refs/heads/pipeline/runs/${payload.run.id}/${payload.workflow.id}`;
15
18
  return {
16
19
  finalRef: `${prefix}/final`,
17
20
  nodeRef: `${prefix}/nodes/${nodeId}`,
18
21
  prefix
19
22
  };
20
- }
21
- async function prepareRunnerGitWorkspace(payload, options = {}) {
22
- return await Effect.runPromise(Effect.provide(prepareRunnerGitWorkspaceEffect(payload, options), GitPorcelainServiceLive));
23
- }
24
- function prepareRunnerGitWorkspaceEffect(payload, options) {
25
- return Effect.gen(function* () {
26
- if (options.cwd) return resolve(options.cwd);
27
- const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
28
- yield* Effect.sync(() => mkdirSync(dirname(worktreePath), { recursive: true }));
29
- yield* runGit(dirname(worktreePath), [
30
- "clone",
31
- "--no-tags",
32
- payload.repository.url,
33
- worktreePath
34
- ]);
35
- yield* runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
36
- return worktreePath;
37
- });
38
- }
39
- async function mergeDependencyRefs(input) {
40
- return await Effect.runPromise(Effect.provide(mergeDependencyRefsEffect(input), GitPorcelainServiceLive));
41
- }
42
- function mergeDependencyRefsEffect(input) {
43
- return Effect.gen(function* () {
44
- yield* configureGitCommitter(input.worktreePath, input.committer);
45
- yield* Effect.forEach(input.dependencyNodeIds, (nodeId) => mergeDependencyRef(input.worktreePath, runnerGitRefs(input.payload, nodeId).nodeRef));
46
- });
47
- }
48
- function mergeDependencyRef(worktreePath, ref) {
49
- return Effect.gen(function* () {
50
- yield* runGit(worktreePath, [
51
- "fetch",
52
- "origin",
53
- ref
54
- ]);
55
- yield* runGit(worktreePath, [
56
- "merge",
57
- "--no-ff",
58
- "--no-edit",
59
- "FETCH_HEAD"
60
- ]);
61
- });
62
- }
63
- async function commitAndPushNodeRef(input) {
64
- return await Effect.runPromise(Effect.provide(commitAndPushNodeRefEffect(input), GitPorcelainServiceLive));
65
- }
66
- function commitAndPushNodeRefEffect(input) {
67
- return Effect.gen(function* () {
68
- yield* commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
69
- const sha = yield* headSha(input.worktreePath);
70
- yield* pushGeneratedRef(input.worktreePath, runnerGitRefs(input.payload, input.nodeId).nodeRef);
71
- return sha;
72
- });
73
- }
74
- /**
75
- * Run a single git command through the runner's authenticated path: per-command
76
- * credential-helper store (basic-auth from the mounted git-credentials) plus
77
- * GIT_TERMINAL_PROMPT=0 so a missing credential fails fast instead of blocking
78
- * forever on an interactive username prompt. This is the ONE git-auth primitive;
79
- * every runner git operation (node delivery, dependency merge, open-pull-request)
80
- * must route through it rather than spawning naked git.
81
- */
82
- async function runAuthenticatedGit(cwd, args) {
83
- return await Effect.runPromise(Effect.provide(runGit(cwd, args), GitPorcelainServiceLive));
84
- }
85
- async function promoteFinalRef(input) {
86
- return await Effect.runPromise(Effect.provide(promoteFinalRefEffect(input), GitPorcelainServiceLive));
87
- }
88
- function promoteFinalRefEffect(input) {
89
- return Effect.gen(function* () {
90
- yield* mergeDependencyRefsEffect({
91
- committer: input.committer,
92
- dependencyNodeIds: input.sourceNodeIds,
93
- payload: input.payload,
94
- worktreePath: input.worktreePath
95
- });
96
- yield* commitChangesIfNeeded(input.worktreePath, "final", input.committer);
97
- const sha = yield* headSha(input.worktreePath);
98
- yield* pushGeneratedRef(input.worktreePath, runnerGitRefs(input.payload, "final").finalRef);
99
- return sha;
100
- });
101
- }
102
- function pushGeneratedRef(worktreePath, ref) {
103
- return Effect.gen(function* () {
104
- yield* runGit(worktreePath, [
105
- "push",
106
- `--force-with-lease=${ref}:${(yield* remoteHeadSha(worktreePath, ref)) ?? ""}`,
107
- "origin",
108
- `HEAD:${ref}`
109
- ]);
110
- });
111
- }
112
- function remoteHeadSha(worktreePath, ref) {
113
- return Effect.map(runGit(worktreePath, [
114
- "ls-remote",
115
- "--heads",
116
- "origin",
117
- ref
118
- ]), (stdout) => parseLsRemoteSha(stdout));
119
- }
120
- function parseLsRemoteSha(stdout) {
121
- const firstLine = stdout.trim().split("\n")[0];
122
- return firstLine ? firstLine.split(LS_REMOTE_FIELD_SEPARATOR_RE)[0] : void 0;
123
- }
124
- function headSha(worktreePath) {
125
- return Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
126
- }
127
- function runnerCommitMessage(nodeId) {
128
- return `chore(pipeline): ${nodeId}`;
129
- }
130
- function commitChangesIfNeeded(worktreePath, nodeId, committer) {
131
- return Effect.gen(function* () {
132
- if ((yield* runGit(worktreePath, [
133
- "status",
134
- "--porcelain",
135
- "--untracked-files=all"
136
- ])).trim().length === 0) return;
137
- yield* runGit(worktreePath, ["add", "--all"]);
138
- yield* configureGitCommitter(worktreePath, committer);
139
- yield* runGit(worktreePath, [
140
- "commit",
141
- "-m",
142
- runnerCommitMessage(nodeId)
143
- ]);
144
- });
145
- }
146
- function configureGitCommitter(worktreePath, committer) {
147
- return Effect.gen(function* () {
148
- yield* runGit(worktreePath, [
149
- "config",
150
- "--local",
151
- "user.name",
152
- committer.name
153
- ]);
154
- yield* runGit(worktreePath, [
155
- "config",
156
- "--local",
157
- "user.email",
158
- committer.email
159
- ]);
160
- });
161
- }
162
- function runnerGitCommandArgs(args, remoteUrl) {
163
- return [...gitCredentialConfigArgs(remoteUrl), ...args];
164
- }
165
- function runGit(cwd, args) {
166
- return Effect.gen(function* () {
167
- const remoteUrl = yield* remoteUrlFromGitArgs(cwd, args);
168
- yield* Effect.try({
169
- try: () => assertSshCredentialsAvailable(remoteUrl),
170
- catch: (error) => error
171
- });
172
- const git = yield* GitPorcelainService;
173
- const commandArgs = runnerGitCommandArgs(args, remoteUrl);
174
- return yield* git.run(cwd, commandArgs, runnerGitEnv(remoteUrl));
23
+ };
24
+ const parseLsRemoteSha = (stdout) => {
25
+ const [firstLine = ""] = stdout.trim().split("\n");
26
+ if (firstLine.length === 0) return Option.none();
27
+ const [sha = ""] = firstLine.split(LS_REMOTE_FIELD_SEPARATOR_RE);
28
+ return Option.some(sha);
29
+ };
30
+ const runnerCommitMessage = (nodeId) => `chore(pipeline): ${nodeId}`;
31
+ const gitCredentialsDir = () => process.env.PIPELINE_GIT_CREDENTIALS_DIR ?? DEFAULT_GIT_CREDENTIALS_DIR;
32
+ const writableGitCredentialStore = () => process.env.PIPELINE_WRITABLE_GIT_CREDENTIAL_STORE ?? WRITABLE_GIT_CREDENTIAL_STORE;
33
+ const writeGitCredentialStore = (credentials, writablePath, host) => {
34
+ mkdirSync(dirname(writablePath), { recursive: true });
35
+ writeFileSync(writablePath, `https://${encodeURIComponent(credentials.username)}:${encodeURIComponent(credentials.password)}@${host}\n`, { mode: 384 });
36
+ chmodSync(writablePath, 384);
37
+ };
38
+ const existingPreparedBasicAuthCredentialStore = (writablePath) => Option.isSome(preparedBasicAuthCredentialStore) && preparedBasicAuthCredentialStore.value.path === writablePath ? Option.some(writablePath) : Option.none();
39
+ const isPreparedBasicAuthCredentialStore = (writablePath, host) => Option.isSome(preparedBasicAuthCredentialStore) && preparedBasicAuthCredentialStore.value.path === writablePath && preparedBasicAuthCredentialStore.value.host === host;
40
+ const errorCode = (error) => error instanceof Error && "code" in error && isStringValue(error.code) ? Option.some(error.code) : Option.none();
41
+ const isReadOnlyFileSystemError = (error) => Option.contains("EROFS")(errorCode(error));
42
+ const isOwnerReadOnly = (path) => {
43
+ const permissions = statSync(path).mode.toString(8).slice(-3);
44
+ return [
45
+ "4",
46
+ "5",
47
+ "6",
48
+ "7"
49
+ ].includes(permissions[0]) && permissions.slice(1) === "00";
50
+ };
51
+ const ensureSshIdentityPermissions = (identityPath) => {
52
+ try {
53
+ chmodSync(identityPath, 256);
54
+ return;
55
+ } catch (error) {
56
+ if (!(isReadOnlyFileSystemError(error) && isOwnerReadOnly(identityPath))) throw error;
57
+ }
58
+ };
59
+ const readCredentialFile = (path) => readFileSync(path, "utf-8").trim();
60
+ const availableBasicAuthCredentials = () => {
61
+ const credentialsDir = gitCredentialsDir();
62
+ const usernamePath = resolve(credentialsDir, "username");
63
+ const passwordPath = resolve(credentialsDir, "password");
64
+ if (!(existsSync(usernamePath) && existsSync(passwordPath))) return Option.none();
65
+ return Option.some({
66
+ password: readCredentialFile(passwordPath),
67
+ username: readCredentialFile(usernamePath)
175
68
  });
176
- }
177
- function assertSshCredentialsAvailable(remoteUrl) {
178
- if (!(remoteUrl && isSshRemote(remoteUrl))) return;
69
+ };
70
+ const remoteArgFromGitArgs = (args) => Option.fromUndefinedOr(args.slice(1).find((arg) => !arg.startsWith("-")));
71
+ const gitRemoteUrl = (cwd, remoteName) => Effect.gen(function* effectBody() {
72
+ const trimmed = (yield* (yield* GitPorcelainService).run(cwd, [
73
+ "remote",
74
+ "get-url",
75
+ remoteName
76
+ ], {
77
+ ...process.env,
78
+ GIT_TERMINAL_PROMPT: "0"
79
+ })).trim();
80
+ return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
81
+ });
82
+ const remoteUrlForName = (cwd, remoteName) => Option.isSome(remoteName) ? gitRemoteUrl(cwd, remoteName.value) : Effect.succeed(MISSING_REMOTE_URL);
83
+ const isHttpRemote = (value) => value.startsWith("http://") || value.startsWith("https://");
84
+ const isScpLikeSshRemote = (value) => SCP_LIKE_SSH_REMOTE_RE.test(value);
85
+ const isRemoteUrl = (value) => value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
86
+ const literalRemoteUrlFromGitArgs = (args) => {
87
+ if (args[0] === "clone") return Option.fromUndefinedOr(args.find((arg) => isRemoteUrl(arg)));
88
+ if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
89
+ const remoteArg = remoteArgFromGitArgs(args);
90
+ if (Option.isSome(remoteArg) && isRemoteUrl(remoteArg.value)) return remoteArg;
91
+ }
92
+ return Option.none();
93
+ };
94
+ const remoteNameFromGitArgs = (args) => {
95
+ if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
96
+ const remoteArg = remoteArgFromGitArgs(args);
97
+ if (Option.isSome(remoteArg) && !isRemoteUrl(remoteArg.value)) return remoteArg;
98
+ }
99
+ return Option.none();
100
+ };
101
+ const remoteUrlFromGitArgs = (cwd, args) => Effect.gen(function* effectBody() {
102
+ const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
103
+ if (Option.isSome(literalRemoteUrl)) return literalRemoteUrl;
104
+ const remoteName = remoteNameFromGitArgs(args);
105
+ return yield* remoteUrlForName(cwd, remoteName);
106
+ });
107
+ const isSshRemote = (value) => value.startsWith("ssh://") || isScpLikeSshRemote(value);
108
+ const assertSshCredentialsAvailable = (remoteUrl) => {
109
+ if (Option.isNone(remoteUrl) || !isSshRemote(remoteUrl.value)) return;
179
110
  const credentialsDir = gitCredentialsDir();
180
111
  const missing = [["identity", resolve(credentialsDir, "identity")], ["known_hosts", resolve(credentialsDir, "known_hosts")]].filter(([, filePath]) => !existsSync(filePath)).map(([name]) => name);
181
112
  if (missing.length === 0) return;
182
- throw new Error(`SSH git remote ${remoteUrl} requires mounted git credential file(s): ${missing.join(", ")}`);
183
- }
184
- function gitCredentialConfigArgs(remoteUrl) {
113
+ throw new Error(`SSH git remote ${remoteUrl.value} requires mounted git credential file(s): ${missing.join(", ")}`);
114
+ };
115
+ const credentialHost = (remoteUrl) => {
116
+ try {
117
+ const { host } = new URL(remoteUrl);
118
+ return host.length > 0 ? Option.some(host) : Option.none();
119
+ } catch {
120
+ return Option.none();
121
+ }
122
+ };
123
+ const prepareBasicAuthCredentialStore = (credentials, writablePath, remoteUrl) => {
124
+ const host = Option.isSome(remoteUrl) ? credentialHost(remoteUrl.value) : Option.none();
125
+ if (Option.isNone(host)) return existingPreparedBasicAuthCredentialStore(writablePath);
126
+ if (isPreparedBasicAuthCredentialStore(writablePath, host.value)) return Option.some(writablePath);
127
+ writeGitCredentialStore(credentials, writablePath, host.value);
128
+ preparedBasicAuthCredentialStore = Option.some({
129
+ host: host.value,
130
+ path: writablePath
131
+ });
132
+ return Option.some(writablePath);
133
+ };
134
+ const prepareWritableGitCredentialStore = (remoteUrl) => {
135
+ if (Option.isNone(remoteUrl)) return existingPreparedBasicAuthCredentialStore(writableGitCredentialStore());
136
+ if (!isHttpRemote(remoteUrl.value)) return Option.none();
137
+ const writablePath = writableGitCredentialStore();
138
+ const basicAuth = availableBasicAuthCredentials();
139
+ if (Option.isSome(basicAuth)) return prepareBasicAuthCredentialStore(basicAuth.value, writablePath, remoteUrl);
140
+ return Option.none();
141
+ };
142
+ const gitCredentialConfigArgs = (remoteUrl) => {
185
143
  const writablePath = prepareWritableGitCredentialStore(remoteUrl);
186
- if (!writablePath) return [];
144
+ if (Option.isNone(writablePath)) return [];
187
145
  return [
188
146
  "-c",
189
147
  "credential.helper=",
190
148
  "-c",
191
- `credential.helper=store --file=${writablePath}`
149
+ `credential.helper=store --file=${writablePath.value}`
192
150
  ];
193
- }
194
- function prepareWritableGitCredentialStore(remoteUrl) {
195
- if (!remoteUrl) return existingPreparedBasicAuthCredentialStore(writableGitCredentialStore());
196
- if (!isHttpRemote(remoteUrl)) return;
197
- const writablePath = writableGitCredentialStore();
198
- const basicAuth = availableBasicAuthCredentials();
199
- if (basicAuth) return prepareBasicAuthCredentialStore(basicAuth, writablePath, remoteUrl);
200
- }
201
- function availableBasicAuthCredentials() {
202
- const credentialsDir = gitCredentialsDir();
203
- const usernamePath = resolve(credentialsDir, "username");
204
- const passwordPath = resolve(credentialsDir, "password");
205
- if (!(existsSync(usernamePath) && existsSync(passwordPath))) return;
206
- return {
207
- password: readCredentialFile(passwordPath),
208
- username: readCredentialFile(usernamePath)
209
- };
210
- }
211
- function gitCredentialsDir() {
212
- return process.env.PIPELINE_GIT_CREDENTIALS_DIR ?? DEFAULT_GIT_CREDENTIALS_DIR;
213
- }
214
- function writableGitCredentialStore() {
215
- return process.env.PIPELINE_WRITABLE_GIT_CREDENTIAL_STORE ?? WRITABLE_GIT_CREDENTIAL_STORE;
216
- }
217
- function writeGitCredentialStore(credentials, writablePath, host) {
218
- mkdirSync(dirname(writablePath), { recursive: true });
219
- writeFileSync(writablePath, `https://${encodeURIComponent(credentials.username)}:${encodeURIComponent(credentials.password)}@${host}\n`, { mode: 384 });
220
- chmodSync(writablePath, 384);
221
- }
222
- function prepareBasicAuthCredentialStore(credentials, writablePath, remoteUrl) {
223
- const host = remoteUrl ? credentialHost(remoteUrl) : void 0;
224
- if (!host) return existingPreparedBasicAuthCredentialStore(writablePath);
225
- if (isPreparedBasicAuthCredentialStore(writablePath, host)) return writablePath;
226
- writeGitCredentialStore(credentials, writablePath, host);
227
- preparedBasicAuthCredentialStore = {
228
- host,
229
- path: writablePath
230
- };
231
- return writablePath;
232
- }
233
- function existingPreparedBasicAuthCredentialStore(writablePath) {
234
- return preparedBasicAuthCredentialStore?.path === writablePath ? writablePath : void 0;
235
- }
236
- function isPreparedBasicAuthCredentialStore(writablePath, host) {
237
- return preparedBasicAuthCredentialStore?.path === writablePath && preparedBasicAuthCredentialStore.host === host;
238
- }
239
- function runnerGitEnv(remoteUrl) {
240
- const env = {
241
- ...process.env,
242
- GIT_TERMINAL_PROMPT: "0"
243
- };
244
- const sshCommand = gitSshCommand();
245
- if (sshCommand && remoteUrl && isSshRemote(remoteUrl)) env.GIT_SSH_COMMAND = sshCommand;
246
- return env;
247
- }
248
- function gitSshCommand() {
151
+ };
152
+ const runnerGitCommandArgs = (args, remoteUrl) => [...gitCredentialConfigArgs(remoteUrl), ...args];
153
+ const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
154
+ const gitSshCommand = () => {
249
155
  const credentialsDir = gitCredentialsDir();
250
156
  const identityPath = resolve(credentialsDir, "identity");
251
157
  const knownHostsPath = resolve(credentialsDir, "known_hosts");
252
- if (!(existsSync(identityPath) && existsSync(knownHostsPath))) return;
158
+ if (!(existsSync(identityPath) && existsSync(knownHostsPath))) return Option.none();
253
159
  ensureSshIdentityPermissions(identityPath);
254
- return [
160
+ return Option.some([
255
161
  "ssh",
256
162
  "-i",
257
163
  shellQuote(identityPath),
@@ -261,90 +167,135 @@ function gitSshCommand() {
261
167
  `UserKnownHostsFile=${shellQuote(knownHostsPath)}`,
262
168
  "-o",
263
169
  "StrictHostKeyChecking=yes"
264
- ].join(" ");
265
- }
266
- function ensureSshIdentityPermissions(identityPath) {
267
- try {
268
- chmodSync(identityPath, 256);
269
- return;
270
- } catch (error) {
271
- if (!(isReadOnlyFileSystemError(error) && isOwnerReadOnly(identityPath))) throw error;
272
- }
273
- }
274
- function isReadOnlyFileSystemError(error) {
275
- return error instanceof Error && "code" in error && error.code === "EROFS";
276
- }
277
- function isOwnerReadOnly(path) {
278
- const permissions = statSync(path).mode.toString(8).slice(-3);
279
- return [
280
- "4",
281
- "5",
282
- "6",
283
- "7"
284
- ].includes(permissions[0] ?? "") && permissions.slice(1) === "00";
285
- }
286
- function readCredentialFile(path) {
287
- return readFileSync(path, "utf8").trim();
288
- }
289
- function remoteUrlFromGitArgs(cwd, args) {
290
- return Effect.gen(function* () {
291
- const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
292
- if (literalRemoteUrl) return literalRemoteUrl;
293
- return yield* remoteUrlForName(cwd, remoteNameFromGitArgs(args));
170
+ ].join(" "));
171
+ };
172
+ const runnerGitEnv = (remoteUrl) => {
173
+ const env = {
174
+ ...process.env,
175
+ GIT_TERMINAL_PROMPT: "0"
176
+ };
177
+ const sshCommand = gitSshCommand();
178
+ if (Option.isSome(sshCommand) && Option.isSome(remoteUrl) && isSshRemote(remoteUrl.value)) env.GIT_SSH_COMMAND = sshCommand.value;
179
+ return env;
180
+ };
181
+ const runGit = (cwd, args) => Effect.gen(function* effectBody() {
182
+ const remoteUrl = yield* remoteUrlFromGitArgs(cwd, args);
183
+ yield* Effect.try({
184
+ catch: (error) => error,
185
+ try: () => {
186
+ assertSshCredentialsAvailable(remoteUrl);
187
+ }
294
188
  });
295
- }
296
- function remoteUrlForName(cwd, remoteName) {
297
- return remoteName ? gitRemoteUrl(cwd, remoteName) : Effect.succeed(void 0);
298
- }
299
- function literalRemoteUrlFromGitArgs(args) {
300
- if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
301
- if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
302
- const remoteArg = remoteArgFromGitArgs(args);
303
- return remoteArg && isRemoteUrl(remoteArg) ? remoteArg : void 0;
304
- }
305
- }
306
- function remoteNameFromGitArgs(args) {
307
- if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
308
- const remoteArg = remoteArgFromGitArgs(args);
309
- if (remoteArg && !remoteArg.startsWith("-") && !isRemoteUrl(remoteArg)) return remoteArg;
310
- }
311
- }
312
- function remoteArgFromGitArgs(args) {
313
- return args.slice(1).find((arg) => !arg.startsWith("-"));
314
- }
315
- function gitRemoteUrl(cwd, remoteName) {
316
- return Effect.gen(function* () {
317
- return (yield* (yield* GitPorcelainService).run(cwd, [
318
- "remote",
319
- "get-url",
320
- remoteName
321
- ], {
322
- ...process.env,
323
- GIT_TERMINAL_PROMPT: "0"
324
- })).trim() || void 0;
189
+ const git = yield* GitPorcelainService;
190
+ const commandArgs = runnerGitCommandArgs(args, remoteUrl);
191
+ return yield* git.run(cwd, commandArgs, runnerGitEnv(remoteUrl));
192
+ });
193
+ const prepareRunnerGitWorkspaceEffect = (payload, options) => Effect.gen(function* effectBody() {
194
+ if (options.cwd !== void 0 && options.cwd.length > 0) return resolve(options.cwd);
195
+ const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
196
+ yield* Effect.sync(() => mkdirSync(dirname(worktreePath), { recursive: true }));
197
+ yield* runGit(dirname(worktreePath), [
198
+ "clone",
199
+ "--no-tags",
200
+ payload.repository.url,
201
+ worktreePath
202
+ ]);
203
+ yield* runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
204
+ return worktreePath;
205
+ });
206
+ const prepareRunnerGitWorkspace = async (payload, options = {}) => await Effect.runPromise(Effect.provide(prepareRunnerGitWorkspaceEffect(payload, options), GitPorcelainServiceLive));
207
+ const mergeDependencyRef = (worktreePath, ref) => Effect.gen(function* effectBody() {
208
+ yield* runGit(worktreePath, [
209
+ "fetch",
210
+ "origin",
211
+ ref
212
+ ]);
213
+ yield* runGit(worktreePath, [
214
+ "merge",
215
+ "--no-ff",
216
+ "--no-edit",
217
+ "FETCH_HEAD"
218
+ ]);
219
+ });
220
+ /**
221
+ * Run a single git command through the runner's authenticated path: per-command
222
+ * credential-helper store (basic-auth from the mounted git-credentials) plus
223
+ * GIT_TERMINAL_PROMPT=0 so a missing credential fails fast instead of blocking
224
+ * forever on an interactive username prompt. This is the ONE git-auth primitive;
225
+ * every runner git operation (node delivery, dependency merge, open-pull-request)
226
+ * must route through it rather than spawning naked git.
227
+ */
228
+ const runAuthenticatedGit = async (cwd, args) => await Effect.runPromise(Effect.provide(runGit(cwd, args), GitPorcelainServiceLive));
229
+ const remoteHeadSha = (worktreePath, ref) => Effect.map(runGit(worktreePath, [
230
+ "ls-remote",
231
+ "--heads",
232
+ "origin",
233
+ ref
234
+ ]), (stdout) => parseLsRemoteSha(stdout));
235
+ const pushGeneratedRef = (worktreePath, ref) => Effect.gen(function* effectBody() {
236
+ const expectedSha = yield* remoteHeadSha(worktreePath, ref);
237
+ yield* runGit(worktreePath, [
238
+ "push",
239
+ `--force-with-lease=${ref}:${Option.getOrElse(expectedSha, () => "")}`,
240
+ "origin",
241
+ `HEAD:${ref}`
242
+ ]);
243
+ });
244
+ const headSha = (worktreePath) => Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
245
+ const configureGitCommitter = (worktreePath, committer) => Effect.gen(function* effectBody() {
246
+ yield* runGit(worktreePath, [
247
+ "config",
248
+ "--local",
249
+ "user.name",
250
+ committer.name
251
+ ]);
252
+ yield* runGit(worktreePath, [
253
+ "config",
254
+ "--local",
255
+ "user.email",
256
+ committer.email
257
+ ]);
258
+ });
259
+ const mergeDependencyRefsEffect = (input) => Effect.gen(function* effectBody() {
260
+ yield* configureGitCommitter(input.worktreePath, input.committer);
261
+ yield* Effect.forEach(input.dependencyNodeIds, (nodeId) => mergeDependencyRef(input.worktreePath, runnerGitRefs(input.payload, nodeId).nodeRef));
262
+ });
263
+ const mergeDependencyRefs = async (input) => {
264
+ await Effect.runPromise(Effect.provide(mergeDependencyRefsEffect(input), GitPorcelainServiceLive));
265
+ };
266
+ const commitChangesIfNeeded = (worktreePath, nodeId, committer) => Effect.gen(function* effectBody() {
267
+ if ((yield* runGit(worktreePath, [
268
+ "status",
269
+ "--porcelain",
270
+ "--untracked-files=all"
271
+ ])).trim().length === 0) return;
272
+ yield* runGit(worktreePath, ["add", "--all"]);
273
+ yield* configureGitCommitter(worktreePath, committer);
274
+ yield* runGit(worktreePath, [
275
+ "commit",
276
+ "-m",
277
+ runnerCommitMessage(nodeId)
278
+ ]);
279
+ });
280
+ const commitAndPushNodeRefEffect = (input) => Effect.gen(function* effectBody() {
281
+ yield* commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
282
+ const sha = yield* headSha(input.worktreePath);
283
+ yield* pushGeneratedRef(input.worktreePath, runnerGitRefs(input.payload, input.nodeId).nodeRef);
284
+ return sha;
285
+ });
286
+ const commitAndPushNodeRef = async (input) => await Effect.runPromise(Effect.provide(commitAndPushNodeRefEffect(input), GitPorcelainServiceLive));
287
+ const promoteFinalRefEffect = (input) => Effect.gen(function* effectBody() {
288
+ yield* mergeDependencyRefsEffect({
289
+ committer: input.committer,
290
+ dependencyNodeIds: input.sourceNodeIds,
291
+ payload: input.payload,
292
+ worktreePath: input.worktreePath
325
293
  });
326
- }
327
- function isRemoteUrl(value) {
328
- return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
329
- }
330
- function isSshRemote(value) {
331
- return value.startsWith("ssh://") || isScpLikeSshRemote(value);
332
- }
333
- function isHttpRemote(value) {
334
- return value.startsWith("http://") || value.startsWith("https://");
335
- }
336
- function isScpLikeSshRemote(value) {
337
- return SCP_LIKE_SSH_REMOTE_RE.test(value);
338
- }
339
- function credentialHost(remoteUrl) {
340
- try {
341
- return new URL(remoteUrl).host || void 0;
342
- } catch {
343
- return;
344
- }
345
- }
346
- function shellQuote(value) {
347
- return `'${value.replaceAll("'", `'\\''`)}'`;
348
- }
294
+ yield* commitChangesIfNeeded(input.worktreePath, "final", input.committer);
295
+ const sha = yield* headSha(input.worktreePath);
296
+ yield* pushGeneratedRef(input.worktreePath, runnerGitRefs(input.payload, "final").finalRef);
297
+ return sha;
298
+ });
299
+ const promoteFinalRef = async (input) => await Effect.runPromise(Effect.provide(promoteFinalRefEffect(input), GitPorcelainServiceLive));
349
300
  //#endregion
350
301
  export { commitAndPushNodeRef, mergeDependencyRefs, prepareRunnerGitWorkspace, promoteFinalRef, runAuthenticatedGit, runnerCommitMessage };
@@ -9,15 +9,15 @@ const OPENCODE_EXCLUDES = [
9
9
  "build/",
10
10
  "coverage/"
11
11
  ];
12
- const LINE_RE = /\r?\n/;
13
- function ensureOpencodeGitExcludes(worktreePath) {
12
+ const LINE_RE = /\r?\n/u;
13
+ const ensureOpencodeGitExcludes = (worktreePath) => {
14
14
  const excludePath = join(worktreePath, ".git", "info", "exclude");
15
15
  if (!existsSync(excludePath)) return;
16
- const existing = readFileSync(excludePath, "utf8");
16
+ const existing = readFileSync(excludePath, "utf-8");
17
17
  const missing = OPENCODE_EXCLUDES.filter((entry) => !existing.split(LINE_RE).includes(entry));
18
18
  if (missing.length === 0) return;
19
19
  mkdirSync(join(worktreePath, ".git", "info"), { recursive: true });
20
20
  appendFileSync(excludePath, `${existing.endsWith("\n") ? "" : "\n"}# oisin-pipeline opencode excludes\n${missing.join("\n")}\n`);
21
- }
21
+ };
22
22
  //#endregion
23
23
  export { ensureOpencodeGitExcludes };