@shipfox/api-workflows 11.0.0 → 12.1.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 (346) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +107 -0
  3. package/README.md +75 -0
  4. package/dist/core/agent-defaults.d.ts +2 -1
  5. package/dist/core/agent-defaults.d.ts.map +1 -1
  6. package/dist/core/agent-defaults.js.map +1 -1
  7. package/dist/core/checkout.d.ts +7 -21
  8. package/dist/core/checkout.d.ts.map +1 -1
  9. package/dist/core/checkout.js +122 -41
  10. package/dist/core/checkout.js.map +1 -1
  11. package/dist/core/entities/job-execution.d.ts +8 -0
  12. package/dist/core/entities/job-execution.d.ts.map +1 -1
  13. package/dist/core/entities/job-execution.js +13 -1
  14. package/dist/core/entities/job-execution.js.map +1 -1
  15. package/dist/core/entities/job-listener-event.d.ts +2 -0
  16. package/dist/core/entities/job-listener-event.d.ts.map +1 -1
  17. package/dist/core/entities/job-listener-event.js.map +1 -1
  18. package/dist/core/entities/step.d.ts +13 -3
  19. package/dist/core/entities/step.d.ts.map +1 -1
  20. package/dist/core/entities/step.js +6 -0
  21. package/dist/core/entities/step.js.map +1 -1
  22. package/dist/core/entities/workflow-run-attempt.d.ts +1 -0
  23. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  24. package/dist/core/entities/workflow-run-attempt.js.map +1 -1
  25. package/dist/core/entities/workflow-run.d.ts +17 -0
  26. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  27. package/dist/core/entities/workflow-run.js.map +1 -1
  28. package/dist/core/errors.d.ts +9 -2
  29. package/dist/core/errors.d.ts.map +1 -1
  30. package/dist/core/errors.js +10 -5
  31. package/dist/core/errors.js.map +1 -1
  32. package/dist/core/job-execution.d.ts.map +1 -1
  33. package/dist/core/job-execution.js +14 -8
  34. package/dist/core/job-execution.js.map +1 -1
  35. package/dist/core/job-transition/decide-job-activation.d.ts +1 -0
  36. package/dist/core/job-transition/decide-job-activation.d.ts.map +1 -1
  37. package/dist/core/job-transition/decide-job-activation.js +1 -0
  38. package/dist/core/job-transition/decide-job-activation.js.map +1 -1
  39. package/dist/core/job-transition/derive-job-success.d.ts +1 -0
  40. package/dist/core/job-transition/derive-job-success.d.ts.map +1 -1
  41. package/dist/core/job-transition/derive-job-success.js +5 -8
  42. package/dist/core/job-transition/derive-job-success.js.map +1 -1
  43. package/dist/core/listener-execution-materialization.d.ts +6 -3
  44. package/dist/core/listener-execution-materialization.d.ts.map +1 -1
  45. package/dist/core/listener-execution-materialization.js +44 -6
  46. package/dist/core/listener-execution-materialization.js.map +1 -1
  47. package/dist/core/resolve-trigger-reference.d.ts +11 -0
  48. package/dist/core/resolve-trigger-reference.d.ts.map +1 -0
  49. package/dist/core/resolve-trigger-reference.js +39 -0
  50. package/dist/core/resolve-trigger-reference.js.map +1 -0
  51. package/dist/core/run-workflow.d.ts +1 -0
  52. package/dist/core/run-workflow.d.ts.map +1 -1
  53. package/dist/core/run-workflow.js +1 -0
  54. package/dist/core/run-workflow.js.map +1 -1
  55. package/dist/core/step-config/agent.d.ts +1 -2
  56. package/dist/core/step-config/agent.d.ts.map +1 -1
  57. package/dist/core/step-config/agent.js +22 -7
  58. package/dist/core/step-config/agent.js.map +1 -1
  59. package/dist/core/step-config/assemble-run-context.d.ts +19 -8
  60. package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
  61. package/dist/core/step-config/assemble-run-context.js +51 -19
  62. package/dist/core/step-config/assemble-run-context.js.map +1 -1
  63. package/dist/core/step-config/complete-step-dispatch-config.d.ts.map +1 -1
  64. package/dist/core/step-config/complete-step-dispatch-config.js +72 -4
  65. package/dist/core/step-config/complete-step-dispatch-config.js.map +1 -1
  66. package/dist/core/step-config/index.d.ts +1 -0
  67. package/dist/core/step-config/index.d.ts.map +1 -1
  68. package/dist/core/step-config/index.js +1 -0
  69. package/dist/core/step-config/index.js.map +1 -1
  70. package/dist/core/step-config/materialize-job-execution-steps.d.ts +1 -1
  71. package/dist/core/step-config/materialize-job-execution-steps.d.ts.map +1 -1
  72. package/dist/core/step-config/materialize-job-execution-steps.js +49 -8
  73. package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
  74. package/dist/core/step-config/materialize-workflow-model.d.ts +3 -3
  75. package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
  76. package/dist/core/step-config/materialize-workflow-model.js +9 -5
  77. package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
  78. package/dist/core/step-config/resolve-job-execution-name.d.ts +3 -3
  79. package/dist/core/step-config/resolve-job-execution-name.d.ts.map +1 -1
  80. package/dist/core/step-config/resolve-job-execution-name.js +24 -13
  81. package/dist/core/step-config/resolve-job-execution-name.js.map +1 -1
  82. package/dist/core/step-config/resolve-step-config.d.ts.map +1 -1
  83. package/dist/core/step-config/resolve-step-config.js +174 -9
  84. package/dist/core/step-config/resolve-step-config.js.map +1 -1
  85. package/dist/core/step-config/resolve-workflow-run-name.d.ts +19 -0
  86. package/dist/core/step-config/resolve-workflow-run-name.d.ts.map +1 -0
  87. package/dist/core/step-config/resolve-workflow-run-name.js +73 -0
  88. package/dist/core/step-config/resolve-workflow-run-name.js.map +1 -0
  89. package/dist/core/step-transition/decide-step-transition.js.map +1 -1
  90. package/dist/core/step-transition/evaluate-gate.d.ts +2 -1
  91. package/dist/core/step-transition/evaluate-gate.d.ts.map +1 -1
  92. package/dist/core/step-transition/evaluate-gate.js +5 -3
  93. package/dist/core/step-transition/evaluate-gate.js.map +1 -1
  94. package/dist/core/workflow-run-creation.d.ts +3 -0
  95. package/dist/core/workflow-run-creation.d.ts.map +1 -1
  96. package/dist/core/workflow-run-creation.js +22 -7
  97. package/dist/core/workflow-run-creation.js.map +1 -1
  98. package/dist/db/db.d.ts +278 -10
  99. package/dist/db/db.d.ts.map +1 -1
  100. package/dist/db/db.js +2 -0
  101. package/dist/db/db.js.map +1 -1
  102. package/dist/db/index.d.ts +3 -2
  103. package/dist/db/index.d.ts.map +1 -1
  104. package/dist/db/index.js +2 -1
  105. package/dist/db/index.js.map +1 -1
  106. package/dist/db/job-listener-events.d.ts +2 -0
  107. package/dist/db/job-listener-events.d.ts.map +1 -1
  108. package/dist/db/job-listener-events.js +1 -0
  109. package/dist/db/job-listener-events.js.map +1 -1
  110. package/dist/db/job-listeners.d.ts +2 -0
  111. package/dist/db/job-listeners.d.ts.map +1 -1
  112. package/dist/db/job-listeners.js +81 -21
  113. package/dist/db/job-listeners.js.map +1 -1
  114. package/dist/db/schema/job-executions.d.ts +3 -3
  115. package/dist/db/schema/job-executions.d.ts.map +1 -1
  116. package/dist/db/schema/job-executions.js +8 -4
  117. package/dist/db/schema/job-executions.js.map +1 -1
  118. package/dist/db/schema/job-listener-events.d.ts +20 -0
  119. package/dist/db/schema/job-listener-events.d.ts.map +1 -1
  120. package/dist/db/schema/job-listener-events.js +2 -0
  121. package/dist/db/schema/job-listener-events.js.map +1 -1
  122. package/dist/db/schema/step-attempts.js +2 -2
  123. package/dist/db/schema/step-attempts.js.map +1 -1
  124. package/dist/db/schema/steps.d.ts +4 -2
  125. package/dist/db/schema/steps.d.ts.map +1 -1
  126. package/dist/db/schema/steps.js +1 -1
  127. package/dist/db/schema/steps.js.map +1 -1
  128. package/dist/db/schema/workflow-run-attempts.d.ts +21 -2
  129. package/dist/db/schema/workflow-run-attempts.d.ts.map +1 -1
  130. package/dist/db/schema/workflow-run-attempts.js +2 -0
  131. package/dist/db/schema/workflow-run-attempts.js.map +1 -1
  132. package/dist/db/schema/workflow-run-counters.d.ts +43 -0
  133. package/dist/db/schema/workflow-run-counters.d.ts.map +1 -0
  134. package/dist/db/schema/workflow-run-counters.js +10 -0
  135. package/dist/db/schema/workflow-run-counters.js.map +1 -0
  136. package/dist/db/schema/workflow-runs.d.ts +54 -1
  137. package/dist/db/schema/workflow-runs.d.ts.map +1 -1
  138. package/dist/db/schema/workflow-runs.js +10 -2
  139. package/dist/db/schema/workflow-runs.js.map +1 -1
  140. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  141. package/dist/db/workflow-runs/job-executions.js +35 -14
  142. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  143. package/dist/db/workflow-runs/jobs.d.ts +2 -0
  144. package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
  145. package/dist/db/workflow-runs/jobs.js +9 -5
  146. package/dist/db/workflow-runs/jobs.js.map +1 -1
  147. package/dist/db/workflow-runs/queries.d.ts +51 -0
  148. package/dist/db/workflow-runs/queries.d.ts.map +1 -1
  149. package/dist/db/workflow-runs/queries.js +82 -2
  150. package/dist/db/workflow-runs/queries.js.map +1 -1
  151. package/dist/db/workflow-runs/run-create.d.ts +1 -0
  152. package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
  153. package/dist/db/workflow-runs/run-create.js +159 -32
  154. package/dist/db/workflow-runs/run-create.js.map +1 -1
  155. package/dist/db/workflow-runs/run-rerun.d.ts.map +1 -1
  156. package/dist/db/workflow-runs/run-rerun.js +13 -5
  157. package/dist/db/workflow-runs/run-rerun.js.map +1 -1
  158. package/dist/db/workflow-runs/shared.d.ts +1 -0
  159. package/dist/db/workflow-runs/shared.d.ts.map +1 -1
  160. package/dist/db/workflow-runs/shared.js +2 -1
  161. package/dist/db/workflow-runs/shared.js.map +1 -1
  162. package/dist/db/workflow-runs/steps.js +3 -3
  163. package/dist/db/workflow-runs/steps.js.map +1 -1
  164. package/dist/db/workflow-runs.d.ts +4 -3
  165. package/dist/db/workflow-runs.d.ts.map +1 -1
  166. package/dist/db/workflow-runs.js +3 -2
  167. package/dist/db/workflow-runs.js.map +1 -1
  168. package/dist/metrics/instance.d.ts +1 -0
  169. package/dist/metrics/instance.d.ts.map +1 -1
  170. package/dist/metrics/instance.js +9 -0
  171. package/dist/metrics/instance.js.map +1 -1
  172. package/dist/presentation/dto/checkout-token.d.ts +1 -0
  173. package/dist/presentation/dto/checkout-token.d.ts.map +1 -1
  174. package/dist/presentation/dto/checkout-token.js +1 -0
  175. package/dist/presentation/dto/checkout-token.js.map +1 -1
  176. package/dist/presentation/dto/index.d.ts +1 -1
  177. package/dist/presentation/dto/index.d.ts.map +1 -1
  178. package/dist/presentation/dto/index.js +1 -1
  179. package/dist/presentation/dto/index.js.map +1 -1
  180. package/dist/presentation/dto/step.d.ts.map +1 -1
  181. package/dist/presentation/dto/step.js +2 -2
  182. package/dist/presentation/dto/step.js.map +1 -1
  183. package/dist/presentation/dto/workflow-run.d.ts +3 -1
  184. package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
  185. package/dist/presentation/dto/workflow-run.js +34 -0
  186. package/dist/presentation/dto/workflow-run.js.map +1 -1
  187. package/dist/presentation/inter-module.d.ts.map +1 -1
  188. package/dist/presentation/inter-module.js +31 -3
  189. package/dist/presentation/inter-module.js.map +1 -1
  190. package/dist/presentation/routes/checkout-token.d.ts +1 -1
  191. package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
  192. package/dist/presentation/routes/checkout-token.js +32 -20
  193. package/dist/presentation/routes/checkout-token.js.map +1 -1
  194. package/dist/presentation/routes/leased-step.d.ts +2 -0
  195. package/dist/presentation/routes/leased-step.d.ts.map +1 -1
  196. package/dist/presentation/routes/leased-step.js +2 -1
  197. package/dist/presentation/routes/leased-step.js.map +1 -1
  198. package/dist/presentation/routes/list-runs.d.ts.map +1 -1
  199. package/dist/presentation/routes/list-runs.js +9 -3
  200. package/dist/presentation/routes/list-runs.js.map +1 -1
  201. package/dist/presentation/routes/project-access.d.ts +1 -0
  202. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  203. package/dist/presentation/routes/report-step.d.ts.map +1 -1
  204. package/dist/presentation/routes/report-step.js +9 -2
  205. package/dist/presentation/routes/report-step.js.map +1 -1
  206. package/dist/presentation/subscribers/on-job-steps-settled.js +1 -1
  207. package/dist/presentation/subscribers/on-job-steps-settled.js.map +1 -1
  208. package/dist/presentation/subscribers/on-runner-job-claimed.d.ts.map +1 -1
  209. package/dist/presentation/subscribers/on-runner-job-claimed.js +21 -0
  210. package/dist/presentation/subscribers/on-runner-job-claimed.js.map +1 -1
  211. package/dist/temporal/activities/index.d.ts.map +1 -1
  212. package/dist/temporal/activities/index.js +2 -1
  213. package/dist/temporal/activities/index.js.map +1 -1
  214. package/dist/temporal/activities/orchestration-activities.d.ts +1 -0
  215. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  216. package/dist/temporal/activities/orchestration-activities.js.map +1 -1
  217. package/dist/temporal/constants.d.ts +1 -0
  218. package/dist/temporal/constants.d.ts.map +1 -1
  219. package/dist/temporal/constants.js +1 -0
  220. package/dist/temporal/constants.js.map +1 -1
  221. package/dist/temporal/index.d.ts +1 -1
  222. package/dist/temporal/index.d.ts.map +1 -1
  223. package/dist/temporal/index.js +1 -1
  224. package/dist/temporal/index.js.map +1 -1
  225. package/dist/temporal/workflows/index.bundle.js +82 -43
  226. package/dist/temporal/workflows/index.d.ts +1 -1
  227. package/dist/temporal/workflows/index.d.ts.map +1 -1
  228. package/dist/temporal/workflows/index.js +1 -1
  229. package/dist/temporal/workflows/index.js.map +1 -1
  230. package/dist/temporal/workflows/job-execution-orchestration.d.ts +5 -0
  231. package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
  232. package/dist/temporal/workflows/job-execution-orchestration.js +77 -43
  233. package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
  234. package/dist/temporal/workflows/test-env.d.ts +5 -1
  235. package/dist/temporal/workflows/test-env.d.ts.map +1 -1
  236. package/dist/temporal/workflows/test-env.js +32 -11
  237. package/dist/temporal/workflows/test-env.js.map +1 -1
  238. package/dist/tsconfig.test.tsbuildinfo +1 -1
  239. package/drizzle/0000_initial.sql +14 -2
  240. package/drizzle/0001_windy_sunfire.sql +1 -0
  241. package/drizzle/meta/0000_snapshot.json +93 -1
  242. package/drizzle/meta/0001_snapshot.json +1711 -0
  243. package/drizzle/meta/_journal.json +2 -2
  244. package/package.json +19 -19
  245. package/src/core/agent-defaults.ts +2 -1
  246. package/src/core/checkout.test.ts +378 -67
  247. package/src/core/checkout.ts +142 -59
  248. package/src/core/entities/job-execution.test.ts +21 -0
  249. package/src/core/entities/job-execution.ts +24 -0
  250. package/src/core/entities/job-listener-event.ts +3 -0
  251. package/src/core/entities/step.ts +15 -3
  252. package/src/core/entities/workflow-run-attempt.ts +1 -0
  253. package/src/core/entities/workflow-run.ts +16 -0
  254. package/src/core/errors.ts +21 -7
  255. package/src/core/job-execution.ts +27 -4
  256. package/src/core/job-transition/decide-job-activation.test.ts +4 -0
  257. package/src/core/job-transition/decide-job-activation.ts +2 -0
  258. package/src/core/job-transition/derive-job-success.test.ts +12 -0
  259. package/src/core/job-transition/derive-job-success.ts +6 -9
  260. package/src/core/listener-execution-materialization.test.ts +2 -0
  261. package/src/core/listener-execution-materialization.ts +57 -11
  262. package/src/core/resolve-trigger-reference.test.ts +160 -0
  263. package/src/core/resolve-trigger-reference.ts +53 -0
  264. package/src/core/run-workflow.ts +2 -0
  265. package/src/core/step-config/agent.ts +29 -12
  266. package/src/core/step-config/assemble-run-context.test.ts +149 -6
  267. package/src/core/step-config/assemble-run-context.ts +87 -25
  268. package/src/core/step-config/complete-step-dispatch-config.test.ts +125 -2
  269. package/src/core/step-config/complete-step-dispatch-config.ts +83 -1
  270. package/src/core/step-config/index.ts +7 -0
  271. package/src/core/step-config/materialize-job-execution-steps.test.ts +250 -1
  272. package/src/core/step-config/materialize-job-execution-steps.ts +57 -10
  273. package/src/core/step-config/materialize-workflow-model.test.ts +212 -4
  274. package/src/core/step-config/materialize-workflow-model.ts +33 -28
  275. package/src/core/step-config/resolve-job-execution-name.test.ts +37 -15
  276. package/src/core/step-config/resolve-job-execution-name.ts +30 -15
  277. package/src/core/step-config/resolve-step-config.ts +175 -10
  278. package/src/core/step-config/resolve-workflow-run-name.test.ts +115 -0
  279. package/src/core/step-config/resolve-workflow-run-name.ts +108 -0
  280. package/src/core/step-transition/decide-step-transition.ts +2 -2
  281. package/src/core/step-transition/evaluate-gate.test.ts +8 -0
  282. package/src/core/step-transition/evaluate-gate.ts +8 -1
  283. package/src/core/workflow-run-creation.test.ts +7 -4
  284. package/src/core/workflow-run-creation.ts +13 -7
  285. package/src/db/db.ts +2 -0
  286. package/src/db/index.ts +6 -0
  287. package/src/db/job-listener-events.test.ts +19 -0
  288. package/src/db/job-listener-events.ts +3 -0
  289. package/src/db/job-listeners.test.ts +187 -5
  290. package/src/db/job-listeners.ts +122 -30
  291. package/src/db/schema/job-executions.ts +12 -5
  292. package/src/db/schema/job-listener-events.ts +3 -0
  293. package/src/db/schema/step-attempts.ts +2 -2
  294. package/src/db/schema/steps.ts +2 -1
  295. package/src/db/schema/workflow-run-attempts.ts +2 -0
  296. package/src/db/schema/workflow-run-counters.ts +13 -0
  297. package/src/db/schema/workflow-runs.ts +11 -2
  298. package/src/db/workflow-runs/job-executions.ts +37 -15
  299. package/src/db/workflow-runs/jobs.ts +17 -4
  300. package/src/db/workflow-runs/queries.test.ts +206 -0
  301. package/src/db/workflow-runs/queries.ts +146 -2
  302. package/src/db/workflow-runs/run-create.test.ts +606 -8
  303. package/src/db/workflow-runs/run-create.ts +185 -40
  304. package/src/db/workflow-runs/run-rerun.test.ts +61 -2
  305. package/src/db/workflow-runs/run-rerun.ts +15 -5
  306. package/src/db/workflow-runs/shared.ts +2 -0
  307. package/src/db/workflow-runs/steps.ts +3 -3
  308. package/src/db/workflow-runs.ts +7 -0
  309. package/src/metrics/instance.ts +14 -0
  310. package/src/presentation/dto/checkout-token.test.ts +19 -9
  311. package/src/presentation/dto/checkout-token.ts +2 -1
  312. package/src/presentation/dto/index.ts +1 -1
  313. package/src/presentation/dto/job.test.ts +9 -0
  314. package/src/presentation/dto/step.test.ts +12 -0
  315. package/src/presentation/dto/step.ts +5 -2
  316. package/src/presentation/dto/workflow-run.ts +44 -2
  317. package/src/presentation/inter-module.test.ts +165 -0
  318. package/src/presentation/inter-module.ts +38 -4
  319. package/src/presentation/routes/checkout-token.test.ts +357 -142
  320. package/src/presentation/routes/checkout-token.ts +56 -26
  321. package/src/presentation/routes/get-run.test.ts +2 -0
  322. package/src/presentation/routes/get-step-secrets.test.ts +7 -2
  323. package/src/presentation/routes/leased-step.ts +9 -1
  324. package/src/presentation/routes/list-runs.test.ts +57 -0
  325. package/src/presentation/routes/list-runs.ts +8 -3
  326. package/src/presentation/routes/report-step.test.ts +25 -0
  327. package/src/presentation/routes/report-step.ts +10 -2
  328. package/src/presentation/subscribers/on-job-steps-settled.ts +1 -1
  329. package/src/presentation/subscribers/on-runner-job-claimed.test.ts +49 -0
  330. package/src/presentation/subscribers/on-runner-job-claimed.ts +22 -0
  331. package/src/temporal/activities/index.ts +1 -0
  332. package/src/temporal/activities/orchestration-activities.ts +1 -0
  333. package/src/temporal/constants.ts +1 -0
  334. package/src/temporal/index.ts +1 -0
  335. package/src/temporal/workflows/index.ts +1 -0
  336. package/src/temporal/workflows/job-execution-orchestration.test.ts +142 -6
  337. package/src/temporal/workflows/job-execution-orchestration.ts +99 -50
  338. package/src/temporal/workflows/run-orchestration.test.ts +1 -1
  339. package/src/temporal/workflows/test-env.ts +50 -13
  340. package/test/factories/project.ts +2 -0
  341. package/test/factories/workflow-model.ts +80 -26
  342. package/test/factories/workflow-run.ts +4 -1
  343. package/test/fixtures/agent-inter-module.ts +2 -1
  344. package/test/fixtures/job-with-steps.ts +1 -1
  345. package/tsconfig.build.tsbuildinfo +1 -1
  346. package/drizzle/0001_agent_tool_materialization.sql +0 -1
@@ -1,5 +1,6 @@
1
1
  import {WORKFLOWS_JOB_EVENT_DELIVERED} from '@shipfox/api-workflows-dto';
2
2
  import {eq} from 'drizzle-orm';
3
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
3
4
  import {db} from '#db/db.js';
4
5
  import {jobListenerEvents} from '#db/schema/job-listener-events.js';
5
6
  import {jobs} from '#db/schema/jobs.js';
@@ -13,6 +14,7 @@ interface DeliverOverrides {
13
14
  disposition?: 'fire' | 'resolve';
14
15
  eventRef?: string;
15
16
  provider?: string;
17
+ triggerReference?: WorkflowRunTriggerReference | null;
16
18
  }
17
19
 
18
20
  function deliver(overrides: DeliverOverrides = {}) {
@@ -24,6 +26,7 @@ function deliver(overrides: DeliverOverrides = {}) {
24
26
  source: 'github',
25
27
  event: 'pull_request_review',
26
28
  provider: overrides.provider ?? 'github',
29
+ triggerReference: overrides.triggerReference,
27
30
  payload: {action: 'submitted'},
28
31
  receivedAt: new Date('2026-01-01T00:00:00.000Z'),
29
32
  });
@@ -56,6 +59,22 @@ describe('deliverEventToListener', () => {
56
59
  expect(rows[0]?.disposition).toBe(disposition);
57
60
  });
58
61
 
62
+ it('persists the normalized trigger reference with the listener event', async () => {
63
+ const job = await jobFactory.create({}, {transient: {status: 'pending'}});
64
+ const triggerReference: WorkflowRunTriggerReference = {
65
+ project: {id: crypto.randomUUID()},
66
+ repository: 'acme/api',
67
+ ref: 'refs/heads/main',
68
+ commit: 'a'.repeat(40),
69
+ actor: 'octocat',
70
+ };
71
+
72
+ await deliver({jobId: job.id, triggerReference});
73
+
74
+ const rows = await listEvents(job.id);
75
+ expect(rows[0]?.triggerReference).toEqual(triggerReference);
76
+ });
77
+
59
78
  it('skips terminal jobs without throwing or buffering', async () => {
60
79
  const job = await jobFactory.create({}, {transient: {status: 'succeeded'}});
61
80
 
@@ -2,6 +2,7 @@ import {WORKFLOWS_JOB_EVENT_DELIVERED} from '@shipfox/api-workflows-dto';
2
2
  import {eq} from 'drizzle-orm';
3
3
  import {isJobTerminal} from '#core/entities/job.js';
4
4
  import type {JobListenerEventDisposition} from '#core/entities/job-listener-event.js';
5
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
5
6
  import {recordListenerEventReceived} from '#metrics/instance.js';
6
7
  import {db} from './db.js';
7
8
  import {writeWorkflowsOutboxEvent} from './outbox-writes.js';
@@ -16,6 +17,7 @@ export interface DeliverEventToListenerParams {
16
17
  source: string;
17
18
  event: string;
18
19
  provider: string;
20
+ triggerReference?: WorkflowRunTriggerReference | null | undefined;
19
21
  payload: unknown;
20
22
  receivedAt: Date;
21
23
  }
@@ -47,6 +49,7 @@ export async function deliverEventToListener(
47
49
  deliveryId: params.deliveryId,
48
50
  source: params.source,
49
51
  event: params.event,
52
+ triggerReference: params.triggerReference ?? null,
50
53
  payload: params.payload,
51
54
  receivedAt: params.receivedAt,
52
55
  })
@@ -6,6 +6,7 @@ import {createWorkflowExpression} from '@shipfox/expression';
6
6
  import {and, asc, eq, isNull} from 'drizzle-orm';
7
7
  import type {JobListeningTrigger, JobStatus} from '#core/entities/job.js';
8
8
  import type {JobExecutionStatus} from '#core/entities/job-execution.js';
9
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
9
10
  import {nextStepForJob, recordStepResult} from '#core/job-execution.js';
10
11
  import {db} from '#db/db.js';
11
12
  import {deliverEventToListener} from '#db/job-listener-events.js';
@@ -83,6 +84,7 @@ function bufferEvent(
83
84
  disposition: 'fire' | 'resolve' = 'fire',
84
85
  eventRef = crypto.randomUUID(),
85
86
  receivedAt = new Date('2026-01-01T00:00:00.000Z'),
87
+ triggerReference?: WorkflowRunTriggerReference | null,
86
88
  ) {
87
89
  return deliverEventToListener({
88
90
  jobId,
@@ -92,6 +94,7 @@ function bufferEvent(
92
94
  source: 'github',
93
95
  event: 'pull_request',
94
96
  provider: 'github',
97
+ triggerReference,
95
98
  payload: {action: 'opened'},
96
99
  receivedAt,
97
100
  });
@@ -324,9 +327,16 @@ describe('activateJobListener', () => {
324
327
  const snapshot = payload.on[0]?.filter_snapshot;
325
328
  expect(snapshot).toEqual({
326
329
  run: expect.objectContaining({id: expect.any(String), name: 'Test Workflow'}),
327
- trigger: {source: 'github', event: 'pull_request'},
330
+ trigger: {
331
+ source: 'github',
332
+ event: 'pull_request',
333
+ project: null,
334
+ repository: null,
335
+ ref: null,
336
+ commit: null,
337
+ },
328
338
  inputs: {environment: 'prod'},
329
- job: {key: 'await'},
339
+ job: {key: 'await', name: 'await'},
330
340
  jobs: {
331
341
  build: expect.objectContaining({
332
342
  status: 'succeeded',
@@ -473,6 +483,69 @@ describe('drainListenerEventsIntoExecution', () => {
473
483
  ]);
474
484
  });
475
485
 
486
+ it('exposes each buffered event reference on its execution event', async () => {
487
+ const job = await createListeningJob({status: 'running', listenerStatus: 'listening'});
488
+ const firstReference: WorkflowRunTriggerReference = {
489
+ project: {id: crypto.randomUUID()},
490
+ repository: 'acme/one',
491
+ ref: 'refs/heads/main',
492
+ commit: '1'.repeat(40),
493
+ actor: 'octocat',
494
+ };
495
+ const secondReference: WorkflowRunTriggerReference = {
496
+ project: {id: crypto.randomUUID()},
497
+ repository: 'acme/two',
498
+ ref: 'refs/pull/12/head',
499
+ commit: '2'.repeat(40),
500
+ actor: 'octocat',
501
+ };
502
+
503
+ await bufferEvent(
504
+ job.id,
505
+ 'fire',
506
+ crypto.randomUUID(),
507
+ new Date('2026-01-01T00:00:00.000Z'),
508
+ firstReference,
509
+ );
510
+ await bufferEvent(
511
+ job.id,
512
+ 'fire',
513
+ crypto.randomUUID(),
514
+ new Date('2026-01-01T00:01:00.000Z'),
515
+ secondReference,
516
+ );
517
+
518
+ await drainListenerEventsIntoExecution({jobId: job.id, expectedSequence: 1});
519
+
520
+ const [execution] = await db()
521
+ .select()
522
+ .from(jobExecutions)
523
+ .where(and(eq(jobExecutions.jobId, job.id), eq(jobExecutions.sequence, 1)));
524
+ // The execution event carries the reference's location fields, not its actor:
525
+ // `WorkflowExecutionEvent` enumerates its own shape and the trigger actor is not part of
526
+ // it. Widening that is the listening-jobs contract's call, not this branch's.
527
+ const {actor: _firstActor, ...firstEventFields} = firstReference;
528
+ const {actor: _secondActor, ...secondEventFields} = secondReference;
529
+ expect(execution?.triggerEvents).toEqual([
530
+ {
531
+ source: 'github',
532
+ event: 'pull_request',
533
+ delivery_id: expect.any(String),
534
+ received_at: '2026-01-01T00:00:00.000Z',
535
+ ...firstEventFields,
536
+ data: {action: 'opened'},
537
+ },
538
+ {
539
+ source: 'github',
540
+ event: 'pull_request',
541
+ delivery_id: expect.any(String),
542
+ received_at: '2026-01-01T00:01:00.000Z',
543
+ ...secondEventFields,
544
+ data: {action: 'opened'},
545
+ },
546
+ ]);
547
+ });
548
+
476
549
  it('materializes a pending execution from buffered fire events and consumes them', async () => {
477
550
  const job = await createListeningJob({status: 'running', listenerStatus: 'listening'});
478
551
  await bufferEvent(job.id);
@@ -493,6 +566,97 @@ describe('drainListenerEventsIntoExecution', () => {
493
566
  expect(events.every((event) => event.consumedByExecutionId === executions[0]?.id)).toBe(true);
494
567
  });
495
568
 
569
+ it('persists a failed execution when listener variables are missing', async () => {
570
+ const job = await createListeningJobFromModel({
571
+ jobs: {
572
+ review: {
573
+ executionName: `Review ${template('vars.REGION')}`,
574
+ steps: [{run: `echo ${template('vars.MISSING')}`}],
575
+ },
576
+ },
577
+ });
578
+ await bufferEvent(job.id);
579
+
580
+ const result = await drainListenerEventsIntoExecution({
581
+ jobId: job.id,
582
+ expectedSequence: 1,
583
+ secrets: {
584
+ getVariablesByNamespace: async () => ({values: {}}),
585
+ },
586
+ });
587
+
588
+ const [execution] = await db()
589
+ .select()
590
+ .from(jobExecutions)
591
+ .where(eq(jobExecutions.jobId, job.id));
592
+ expect(result).toMatchObject({kind: 'execution', sequence: 1, status: 'failed'});
593
+ expect(execution).toMatchObject({
594
+ status: 'failed',
595
+ name: null,
596
+ evaluationTrace: [
597
+ expect.objectContaining({
598
+ expression: 'vars.MISSING',
599
+ roots: ['vars'],
600
+ fillTarget: 'execution-creation',
601
+ evaluatedAt: 'execution-creation',
602
+ field: 'run',
603
+ degraded: true,
604
+ }),
605
+ ],
606
+ });
607
+ });
608
+
609
+ it('resolves a distinct name for each listener firing with boundary variables', async () => {
610
+ const job = await createListeningJobFromModel({
611
+ jobs: {
612
+ review: {
613
+ name: 'Process review',
614
+ executionName: `Review ${template('vars.REGION')} ${template('execution.index')}`,
615
+ steps: [{run: 'echo review'}],
616
+ },
617
+ },
618
+ });
619
+ await db().update(jobs).set({name: 'Process review'}).where(eq(jobs.id, job.id));
620
+ const secrets = {
621
+ getVariablesByNamespace: async () => ({values: {REGION: 'eu-west'}}),
622
+ };
623
+ await bufferEvent(job.id);
624
+ await drainListenerEventsIntoExecution({
625
+ jobId: job.id,
626
+ expectedSequence: 1,
627
+ secrets,
628
+ });
629
+ await bufferEvent(job.id);
630
+ await drainListenerEventsIntoExecution({
631
+ jobId: job.id,
632
+ expectedSequence: 2,
633
+ secrets,
634
+ });
635
+
636
+ const executions = await db()
637
+ .select()
638
+ .from(jobExecutions)
639
+ .where(eq(jobExecutions.jobId, job.id))
640
+ .orderBy(asc(jobExecutions.sequence));
641
+ const stored = await readJob(job.id);
642
+
643
+ expect(stored?.name).toBe('Process review');
644
+ expect(executions.map((execution) => execution.name)).toEqual([
645
+ 'Review eu-west 0',
646
+ 'Review eu-west 1',
647
+ ]);
648
+ expect(executions.map((execution) => execution.evaluationTrace)).toEqual([
649
+ expect.arrayContaining([
650
+ expect.objectContaining({field: 'job.execution_name', expression: 'vars.REGION'}),
651
+ expect.objectContaining({field: 'job.execution_name', expression: 'execution.index'}),
652
+ ]),
653
+ expect.arrayContaining([
654
+ expect.objectContaining({field: 'job.execution_name', expression: 'vars.REGION'}),
655
+ expect.objectContaining({field: 'job.execution_name', expression: 'execution.index'}),
656
+ ]),
657
+ ]);
658
+ });
659
+
496
660
  it('uses the frozen agent tool materialization snapshot for listener executions', async () => {
497
661
  const model = workflowModel({
498
662
  jobs: {
@@ -823,12 +987,30 @@ describe('drainListenerEventsIntoExecution', () => {
823
987
  expect(stored?.outputs).toEqual({message: 'hello'});
824
988
  });
825
989
 
826
- it('reports empty when nothing is buffered', async () => {
827
- const job = await createListeningJob({status: 'running', listenerStatus: 'listening'});
990
+ it('reports empty without loading external state when nothing is buffered', async () => {
991
+ const job = await createListeningJobFromModel({
992
+ jobs: {
993
+ review: {
994
+ executionName: template('vars.REGION'),
995
+ steps: [{run: 'echo review'}],
996
+ },
997
+ },
998
+ });
999
+ let variableLoads = 0;
828
1000
 
829
- const result = await drainListenerEventsIntoExecution({jobId: job.id, expectedSequence: 1});
1001
+ const result = await drainListenerEventsIntoExecution({
1002
+ jobId: job.id,
1003
+ expectedSequence: 1,
1004
+ secrets: {
1005
+ getVariablesByNamespace: () => {
1006
+ variableLoads += 1;
1007
+ return Promise.resolve({values: {REGION: 'eu-west'}});
1008
+ },
1009
+ },
1010
+ });
830
1011
 
831
1012
  expect(result).toEqual({kind: 'empty'});
1013
+ expect(variableLoads).toBe(0);
832
1014
  });
833
1015
 
834
1016
  it('returns the existing execution when the sequence was already materialized', async () => {
@@ -2,6 +2,7 @@ import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
2
  import {readPersistedWorkflowModel} from '@shipfox/api-definitions-dto';
3
3
  import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
4
4
  import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
5
+ import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
5
6
  import {
6
7
  WORKFLOWS_JOB_ACTIVATED,
7
8
  type WorkflowsJobActivatedEventDto,
@@ -10,7 +11,13 @@ import {and, asc, count, eq, inArray, isNull, notInArray, sql} from 'drizzle-orm
10
11
  import {type AgentDefaultsResolver, createAgentDefaultsResolver} from '#core/agent-defaults.js';
11
12
  import {loadAgentToolMaterializationContext} from '#core/agent-tools.js';
12
13
  import {isJobTerminal, type JobStatus, type ResolutionReason} from '#core/entities/job.js';
13
- import type {JobExecutionStatus, WorkflowExecutionEvent} from '#core/entities/job-execution.js';
14
+ import type {
15
+ JobExecution,
16
+ JobExecutionStatus,
17
+ WorkflowExecutionEvent,
18
+ } from '#core/entities/job-execution.js';
19
+ import {normalizeWorkflowExecutionEvent} from '#core/entities/job-execution.js';
20
+ import {InterpolationUnresolvableError} from '#core/errors.js';
14
21
  import {type DeriveJobSuccessResult, deriveJobSuccess} from '#core/job-transition/index.js';
15
22
  import {
16
23
  type MaterializedListenerExecution,
@@ -38,6 +45,7 @@ import {toWorkflowRun, workflowRuns} from './schema/workflow-runs.js';
38
45
  import {
39
46
  bulkUpdateStepStatuses,
40
47
  getDirectDependencyJobContexts,
48
+ loadReferencedVariables,
41
49
  updateJobStatusAtVersion,
42
50
  } from './workflow-runs.js';
43
51
 
@@ -74,7 +82,7 @@ export async function activateJobListener(
74
82
  ): Promise<ActivateJobListenerResult> {
75
83
  return await db().transaction(async (tx) => {
76
84
  const [target] = await tx
77
- .select({job: jobs, run: workflowRuns})
85
+ .select({job: jobs, attempt: workflowRunAttempts, run: workflowRuns})
78
86
  .from(jobs)
79
87
  .innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
80
88
  .innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
@@ -138,6 +146,7 @@ export async function activateJobListener(
138
146
  run: toWorkflowRun(target.run),
139
147
  triggerPayload: target.run.triggerPayload,
140
148
  inputs: target.run.inputs,
149
+ vars: target.attempt.vars ?? undefined,
141
150
  plan: snapshotPlan,
142
151
  dependencyJobs,
143
152
  });
@@ -182,11 +191,61 @@ export interface DrainListenerEventsParams {
182
191
  projects?: ProjectsModuleClient | undefined;
183
192
  resolveAgentDefaults?: AgentDefaultsResolver | undefined;
184
193
  agent?: AgentInterModuleClient | undefined;
194
+ secrets?: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'> | undefined;
185
195
  }
186
196
 
187
197
  export async function drainListenerEventsIntoExecution(
188
198
  params: DrainListenerEventsParams,
189
199
  ): Promise<DrainListenerEventsResult> {
200
+ const probeResult = await db().transaction(async (tx) => {
201
+ const existing = await findExistingExecution(params, tx);
202
+ if (existing) return existing;
203
+
204
+ const resolveRequested = await hasPendingResolveEvent(params.jobId, tx);
205
+ if (resolveRequested) return {kind: 'resolve-requested' as const};
206
+
207
+ const hasBufferedFire = await hasBufferedFireEvent(params.jobId, tx);
208
+ return hasBufferedFire ? null : {kind: 'empty' as const};
209
+ });
210
+ if (probeResult !== null) return probeResult;
211
+
212
+ // Resolve external state before opening the persistence transaction. The
213
+ // secrets module uses the shared pool and cannot reuse this transaction's
214
+ // connection across the inter-module boundary.
215
+ const materializationTarget = await loadListenerMaterializationTarget(params.jobId);
216
+ const model =
217
+ materializationTarget.attempt.model === null
218
+ ? null
219
+ : readPersistedWorkflowModel(materializationTarget.attempt.model);
220
+ const modelJob = model?.jobs.find((job) => job.key === materializationTarget.job.key);
221
+ let vars: Record<string, string> | undefined;
222
+ let variableResolutionError: InterpolationUnresolvableError | undefined;
223
+ if (model !== null && modelJob !== undefined) {
224
+ try {
225
+ vars = await loadReferencedVariables({
226
+ model,
227
+ jobs: [modelJob],
228
+ workspaceId: materializationTarget.run.workspaceId,
229
+ projectId: materializationTarget.run.projectId,
230
+ definitionId: materializationTarget.run.definitionId,
231
+ secrets: params.secrets,
232
+ });
233
+ } catch (error) {
234
+ if (!(error instanceof InterpolationUnresolvableError)) throw error;
235
+ variableResolutionError = error;
236
+ }
237
+ }
238
+ const agentToolContext =
239
+ materializationTarget.attempt.agentToolMaterialization === null
240
+ ? await loadAgentToolMaterializationContext({
241
+ model,
242
+ workspaceId: materializationTarget.run.workspaceId,
243
+ projectId: materializationTarget.run.projectId,
244
+ integrations: params.integrations,
245
+ projects: params.projects,
246
+ })
247
+ : undefined;
248
+
190
249
  const drained = await db().transaction(async (tx) => {
191
250
  const existing = await findExistingExecution(params, tx);
192
251
  if (existing) return {result: existing};
@@ -198,25 +257,20 @@ export async function drainListenerEventsIntoExecution(
198
257
  if (bufferedEvents.length === 0) return {result: {kind: 'empty' as const}};
199
258
 
200
259
  const target = await loadListenerMaterializationTarget(params.jobId, tx);
201
- const model =
202
- target.attempt.model === null ? null : readPersistedWorkflowModel(target.attempt.model);
203
- const agentToolContext =
204
- target.attempt.agentToolMaterialization === null
205
- ? await loadAgentToolMaterializationContext({
206
- model,
207
- workspaceId: target.run.workspaceId,
208
- projectId: target.run.projectId,
209
- integrations: params.integrations,
210
- projects: params.projects,
211
- })
212
- : undefined;
260
+ const priorExecutions = await loadListenerPriorExecutions(
261
+ params.jobId,
262
+ target.job.name ?? target.job.key,
263
+ tx,
264
+ );
213
265
  const materialized = await materializeListenerExecution({
214
266
  model,
215
267
  run: toWorkflowRun(target.run),
216
268
  job: toJob(target.job),
269
+ vars,
270
+ variableResolutionError,
217
271
  sequence: params.expectedSequence,
218
272
  triggerEvents: listenerTriggerEvents(bufferedEvents),
219
- priorExecutions: target.priorExecutions,
273
+ priorExecutions,
220
274
  resolveAgentDefaults:
221
275
  params.resolveAgentDefaults ??
222
276
  (params.agent
@@ -315,7 +369,13 @@ type ApplyJobListenerResolutionResult =
315
369
  async function deriveJobListenerResolutionDecision(
316
370
  jobId: string,
317
371
  ): Promise<JobListenerResolutionDecision> {
318
- const [jobRow] = await db().select().from(jobs).where(eq(jobs.id, jobId)).limit(1);
372
+ const [target] = await db()
373
+ .select({job: jobs, attempt: workflowRunAttempts})
374
+ .from(jobs)
375
+ .innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
376
+ .where(eq(jobs.id, jobId))
377
+ .limit(1);
378
+ const jobRow = target?.job;
319
379
  if (!jobRow) throw new Error(`Job not found: ${jobId}`);
320
380
 
321
381
  const [executionRows, dependencyJobs] = await Promise.all([
@@ -330,8 +390,11 @@ async function deriveJobListenerResolutionDecision(
330
390
  expectedVersion: jobRow.version,
331
391
  ...deriveJobSuccess({
332
392
  success: jobRow.success,
333
- executions: executionRows.map(toJobExecution),
393
+ executions: executionRows.map((execution) =>
394
+ toJobExecution(execution, jobRow.name ?? jobRow.key),
395
+ ),
334
396
  jobs: dependencyJobs,
397
+ vars: target.attempt.vars ?? undefined,
335
398
  }),
336
399
  };
337
400
  }
@@ -462,6 +525,21 @@ async function hasPendingResolveEvent(jobId: string, tx: Tx): Promise<boolean> {
462
525
  return resolveEvent !== undefined;
463
526
  }
464
527
 
528
+ async function hasBufferedFireEvent(jobId: string, tx: Tx): Promise<boolean> {
529
+ const [fireEvent] = await tx
530
+ .select({id: jobListenerEvents.id})
531
+ .from(jobListenerEvents)
532
+ .where(
533
+ and(
534
+ eq(jobListenerEvents.jobId, jobId),
535
+ eq(jobListenerEvents.disposition, 'fire'),
536
+ isNull(jobListenerEvents.consumedByExecutionId),
537
+ ),
538
+ )
539
+ .limit(1);
540
+ return fireEvent !== undefined;
541
+ }
542
+
465
543
  async function lockBufferedFireEvents(
466
544
  params: DrainListenerEventsParams,
467
545
  tx: Tx,
@@ -486,13 +564,19 @@ async function lockBufferedFireEvents(
486
564
  function listenerTriggerEvents(
487
565
  bufferedEvents: readonly JobListenerEventDb[],
488
566
  ): WorkflowExecutionEvent[] {
489
- return bufferedEvents.map((event) => ({
490
- source: event.source,
491
- event: event.event,
492
- delivery_id: event.deliveryId,
493
- received_at: event.receivedAt.toISOString(),
494
- data: event.payload,
495
- }));
567
+ return bufferedEvents.map((event) =>
568
+ normalizeWorkflowExecutionEvent({
569
+ source: event.source,
570
+ event: event.event,
571
+ delivery_id: event.deliveryId,
572
+ received_at: event.receivedAt.toISOString(),
573
+ project: event.triggerReference?.project ?? null,
574
+ repository: event.triggerReference?.repository ?? null,
575
+ ref: event.triggerReference?.ref ?? null,
576
+ commit: event.triggerReference?.commit ?? null,
577
+ data: event.payload,
578
+ }),
579
+ );
496
580
  }
497
581
 
498
582
  async function persistMaterializedListenerExecution(
@@ -509,7 +593,7 @@ async function persistMaterializedListenerExecution(
509
593
  .values({
510
594
  jobId: params.jobId,
511
595
  sequence: params.sequence,
512
- name: params.materialized.name,
596
+ name: params.materialized.nameOverride,
513
597
  runner: params.materialized.runner.length === 0 ? null : [...params.materialized.runner],
514
598
  status: params.materialized.status,
515
599
  statusReason: params.materialized.statusReason,
@@ -559,21 +643,29 @@ function drainExecutionResult(
559
643
  };
560
644
  }
561
645
 
562
- async function loadListenerMaterializationTarget(jobId: string, tx: Tx) {
563
- const [target] = await tx
646
+ async function loadListenerMaterializationTarget(jobId: string, tx?: Tx) {
647
+ const query = (tx ?? db())
564
648
  .select({job: jobs, attempt: workflowRunAttempts, run: workflowRuns})
565
649
  .from(jobs)
566
650
  .innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
567
651
  .innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
568
652
  .where(eq(jobs.id, jobId))
569
- .limit(1)
570
- .for('update');
653
+ .limit(1);
654
+ const [target] = tx === undefined ? await query : await query.for('update');
571
655
  if (!target) throw new Error(`Job not found: ${jobId}`);
572
656
 
657
+ return target;
658
+ }
659
+
660
+ async function loadListenerPriorExecutions(
661
+ jobId: string,
662
+ fallbackName: string,
663
+ tx: Tx,
664
+ ): Promise<JobExecution[]> {
573
665
  const priorExecutions = await tx
574
666
  .select()
575
667
  .from(jobExecutions)
576
668
  .where(eq(jobExecutions.jobId, jobId))
577
669
  .orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
578
- return {...target, priorExecutions: priorExecutions.map(toJobExecution)};
670
+ return priorExecutions.map((execution) => toJobExecution(execution, fallbackName));
579
671
  }
@@ -11,7 +11,11 @@ import {
11
11
  uuid,
12
12
  } from 'drizzle-orm/pg-core';
13
13
  import {toJobStatusReason} from '#core/entities/job.js';
14
- import type {JobExecution, WorkflowExecutionEvent} from '#core/entities/job-execution.js';
14
+ import {
15
+ type JobExecution,
16
+ normalizeWorkflowExecutionEvent,
17
+ type WorkflowExecutionEvent,
18
+ } from '#core/entities/job-execution.js';
15
19
  import type {PersistedEvaluationTraceEntry} from '#core/entities/step.js';
16
20
  import {pgTable} from './common.js';
17
21
  import {jobStatusReasonEnum, jobs} from './jobs.js';
@@ -32,7 +36,9 @@ export const jobExecutions = pgTable(
32
36
  .notNull()
33
37
  .references(() => jobs.id, {onDelete: 'cascade'}),
34
38
  sequence: integer('sequence').notNull(),
35
- name: text('name').notNull(),
39
+ // A null value means no dynamic override was resolved. The core entity
40
+ // exposes the parent job's static name/key as the effective name.
41
+ name: text('name'),
36
42
  runner: jsonb('runner').$type<string[]>(),
37
43
  status: jobExecutionStatusEnum('status').notNull().default('pending'),
38
44
  statusReason: jobStatusReasonEnum('status_reason'),
@@ -62,16 +68,17 @@ export const jobExecutions = pgTable(
62
68
  export type JobExecutionDb = typeof jobExecutions.$inferSelect;
63
69
  export type JobExecutionCreateDb = typeof jobExecutions.$inferInsert;
64
70
 
65
- export function toJobExecution(row: JobExecutionDb): JobExecution {
71
+ export function toJobExecution(row: JobExecutionDb, fallbackName: string): JobExecution {
66
72
  return {
67
73
  id: row.id,
68
74
  jobId: row.jobId,
69
75
  sequence: row.sequence,
70
- name: row.name,
76
+ nameOverride: row.name,
77
+ name: row.name ?? fallbackName,
71
78
  runner: row.runner as string[] | null,
72
79
  status: row.status,
73
80
  statusReason: toJobStatusReason(row.statusReason),
74
- triggerEvents: row.triggerEvents,
81
+ triggerEvents: row.triggerEvents.map(normalizeWorkflowExecutionEvent),
75
82
  outputs: row.outputs,
76
83
  evaluationTrace: row.evaluationTrace ?? null,
77
84
  version: row.version,
@@ -1,6 +1,7 @@
1
1
  import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
2
2
  import {index, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
3
3
  import type {JobListenerEvent} from '#core/entities/job-listener-event.js';
4
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
4
5
  import {pgTable} from './common.js';
5
6
  import {jobExecutions} from './job-executions.js';
6
7
  import {jobs} from './jobs.js';
@@ -22,6 +23,7 @@ export const jobListenerEvents = pgTable(
22
23
  deliveryId: text('delivery_id').notNull(),
23
24
  source: text('source').notNull(),
24
25
  event: text('event').notNull(),
26
+ triggerReference: jsonb('trigger_reference').$type<WorkflowRunTriggerReference>(),
25
27
  payload: jsonb('payload').notNull(),
26
28
  receivedAt: timestamp('received_at', {withTimezone: true}).notNull(),
27
29
  consumedByExecutionId: uuid('consumed_by_execution_id').references(() => jobExecutions.id, {
@@ -50,6 +52,7 @@ export function toJobListenerEvent(row: JobListenerEventDb): JobListenerEvent {
50
52
  deliveryId: row.deliveryId,
51
53
  source: row.source,
52
54
  event: row.event,
55
+ triggerReference: row.triggerReference ?? null,
53
56
  payload: row.payload,
54
57
  receivedAt: row.receivedAt,
55
58
  consumedByExecutionId: row.consumedByExecutionId,
@@ -21,8 +21,8 @@ import {stepStatusEnum, steps} from './steps.js';
21
21
 
22
22
  // Append-only execution history. One row per dispatched attempt of a step,
23
23
  // inserted `running` at dispatch and finalized terminal at report. `steps` holds
24
- // the fast current projection; this table is the audit trail and — via the
25
- // unique (step_id, attempt) constraint the idempotency anchor.
24
+ // the fast current projection; this table is the audit trail. The unique
25
+ // (step_id, attempt) constraint is the idempotency anchor.
26
26
  export const stepAttempts = pgTable(
27
27
  'step_attempts',
28
28
  {
@@ -18,6 +18,7 @@ import {
18
18
  STEP_STATUS_REASONS,
19
19
  type Step,
20
20
  type StepConfigDispatchPlan,
21
+ type StepType,
21
22
  toStepStatusReason,
22
23
  } from '#core/entities/step.js';
23
24
  import {pgTable} from './common.js';
@@ -45,7 +46,7 @@ export const steps = pgTable(
45
46
  status: stepStatusEnum('status').notNull().default('pending'),
46
47
  statusReason: stepStatusReasonEnum('status_reason'),
47
48
  evaluationTrace: jsonb('evaluation_trace').$type<readonly PersistedEvaluationTraceEntry[]>(),
48
- type: text('type').notNull(),
49
+ type: text('type').notNull().$type<StepType>(),
49
50
  config: jsonb('config').notNull().$type<Record<string, unknown>>(),
50
51
  condition: jsonb('condition').$type<WorkflowExpression>(),
51
52
  configPlan: jsonb('config_plan').$type<StepConfigDispatchPlan>(),