@shipfox/api-workflows 10.2.0 → 12.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 (354) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +117 -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/routes/require-accessible-run.d.ts.map +1 -1
  207. package/dist/presentation/routes/require-accessible-run.js +1 -1
  208. package/dist/presentation/routes/require-accessible-run.js.map +1 -1
  209. package/dist/presentation/subscribers/on-job-steps-settled.js +1 -1
  210. package/dist/presentation/subscribers/on-job-steps-settled.js.map +1 -1
  211. package/dist/presentation/subscribers/on-runner-job-claimed.d.ts.map +1 -1
  212. package/dist/presentation/subscribers/on-runner-job-claimed.js +21 -0
  213. package/dist/presentation/subscribers/on-runner-job-claimed.js.map +1 -1
  214. package/dist/temporal/activities/index.d.ts.map +1 -1
  215. package/dist/temporal/activities/index.js +2 -1
  216. package/dist/temporal/activities/index.js.map +1 -1
  217. package/dist/temporal/activities/orchestration-activities.d.ts +1 -0
  218. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  219. package/dist/temporal/activities/orchestration-activities.js.map +1 -1
  220. package/dist/temporal/constants.d.ts +1 -0
  221. package/dist/temporal/constants.d.ts.map +1 -1
  222. package/dist/temporal/constants.js +1 -0
  223. package/dist/temporal/constants.js.map +1 -1
  224. package/dist/temporal/index.d.ts +1 -1
  225. package/dist/temporal/index.d.ts.map +1 -1
  226. package/dist/temporal/index.js +1 -1
  227. package/dist/temporal/index.js.map +1 -1
  228. package/dist/temporal/workflows/index.bundle.js +82 -43
  229. package/dist/temporal/workflows/index.d.ts +1 -1
  230. package/dist/temporal/workflows/index.d.ts.map +1 -1
  231. package/dist/temporal/workflows/index.js +1 -1
  232. package/dist/temporal/workflows/index.js.map +1 -1
  233. package/dist/temporal/workflows/job-execution-orchestration.d.ts +5 -0
  234. package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
  235. package/dist/temporal/workflows/job-execution-orchestration.js +77 -43
  236. package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
  237. package/dist/temporal/workflows/test-env.d.ts +5 -1
  238. package/dist/temporal/workflows/test-env.d.ts.map +1 -1
  239. package/dist/temporal/workflows/test-env.js +32 -11
  240. package/dist/temporal/workflows/test-env.js.map +1 -1
  241. package/dist/tsconfig.test.tsbuildinfo +1 -1
  242. package/drizzle/0000_initial.sql +14 -2
  243. package/drizzle/0001_windy_sunfire.sql +1 -0
  244. package/drizzle/meta/0000_snapshot.json +93 -1
  245. package/drizzle/meta/0001_snapshot.json +1711 -0
  246. package/drizzle/meta/_journal.json +2 -2
  247. package/package.json +21 -21
  248. package/src/core/agent-defaults.ts +2 -1
  249. package/src/core/checkout.test.ts +378 -67
  250. package/src/core/checkout.ts +142 -59
  251. package/src/core/entities/job-execution.test.ts +21 -0
  252. package/src/core/entities/job-execution.ts +24 -0
  253. package/src/core/entities/job-listener-event.ts +3 -0
  254. package/src/core/entities/step.ts +15 -3
  255. package/src/core/entities/workflow-run-attempt.ts +1 -0
  256. package/src/core/entities/workflow-run.ts +16 -0
  257. package/src/core/errors.ts +21 -7
  258. package/src/core/job-execution.ts +27 -4
  259. package/src/core/job-transition/decide-job-activation.test.ts +4 -0
  260. package/src/core/job-transition/decide-job-activation.ts +2 -0
  261. package/src/core/job-transition/derive-job-success.test.ts +12 -0
  262. package/src/core/job-transition/derive-job-success.ts +6 -9
  263. package/src/core/listener-execution-materialization.test.ts +2 -0
  264. package/src/core/listener-execution-materialization.ts +57 -11
  265. package/src/core/resolve-trigger-reference.test.ts +160 -0
  266. package/src/core/resolve-trigger-reference.ts +53 -0
  267. package/src/core/run-workflow.ts +2 -0
  268. package/src/core/step-config/agent.ts +29 -12
  269. package/src/core/step-config/assemble-run-context.test.ts +149 -6
  270. package/src/core/step-config/assemble-run-context.ts +87 -25
  271. package/src/core/step-config/complete-step-dispatch-config.test.ts +125 -2
  272. package/src/core/step-config/complete-step-dispatch-config.ts +83 -1
  273. package/src/core/step-config/index.ts +7 -0
  274. package/src/core/step-config/materialize-job-execution-steps.test.ts +250 -1
  275. package/src/core/step-config/materialize-job-execution-steps.ts +57 -10
  276. package/src/core/step-config/materialize-workflow-model.test.ts +212 -4
  277. package/src/core/step-config/materialize-workflow-model.ts +33 -28
  278. package/src/core/step-config/resolve-job-execution-name.test.ts +37 -15
  279. package/src/core/step-config/resolve-job-execution-name.ts +30 -15
  280. package/src/core/step-config/resolve-step-config.ts +175 -10
  281. package/src/core/step-config/resolve-workflow-run-name.test.ts +115 -0
  282. package/src/core/step-config/resolve-workflow-run-name.ts +108 -0
  283. package/src/core/step-transition/decide-step-transition.ts +2 -2
  284. package/src/core/step-transition/evaluate-gate.test.ts +8 -0
  285. package/src/core/step-transition/evaluate-gate.ts +8 -1
  286. package/src/core/workflow-run-creation.test.ts +7 -4
  287. package/src/core/workflow-run-creation.ts +13 -7
  288. package/src/db/db.ts +2 -0
  289. package/src/db/index.ts +6 -0
  290. package/src/db/job-listener-events.test.ts +19 -0
  291. package/src/db/job-listener-events.ts +3 -0
  292. package/src/db/job-listeners.test.ts +187 -5
  293. package/src/db/job-listeners.ts +122 -30
  294. package/src/db/schema/job-executions.ts +12 -5
  295. package/src/db/schema/job-listener-events.ts +3 -0
  296. package/src/db/schema/step-attempts.ts +2 -2
  297. package/src/db/schema/steps.ts +2 -1
  298. package/src/db/schema/workflow-run-attempts.ts +2 -0
  299. package/src/db/schema/workflow-run-counters.ts +13 -0
  300. package/src/db/schema/workflow-runs.ts +11 -2
  301. package/src/db/workflow-runs/job-executions.ts +37 -15
  302. package/src/db/workflow-runs/jobs.ts +17 -4
  303. package/src/db/workflow-runs/queries.test.ts +206 -0
  304. package/src/db/workflow-runs/queries.ts +146 -2
  305. package/src/db/workflow-runs/run-create.test.ts +606 -8
  306. package/src/db/workflow-runs/run-create.ts +185 -40
  307. package/src/db/workflow-runs/run-rerun.test.ts +61 -2
  308. package/src/db/workflow-runs/run-rerun.ts +15 -5
  309. package/src/db/workflow-runs/shared.ts +2 -0
  310. package/src/db/workflow-runs/steps.ts +3 -3
  311. package/src/db/workflow-runs.ts +7 -0
  312. package/src/metrics/instance.ts +14 -0
  313. package/src/presentation/dto/checkout-token.test.ts +19 -9
  314. package/src/presentation/dto/checkout-token.ts +2 -1
  315. package/src/presentation/dto/index.ts +1 -1
  316. package/src/presentation/dto/job.test.ts +9 -0
  317. package/src/presentation/dto/step.test.ts +12 -0
  318. package/src/presentation/dto/step.ts +5 -2
  319. package/src/presentation/dto/workflow-run.ts +44 -2
  320. package/src/presentation/inter-module.test.ts +165 -0
  321. package/src/presentation/inter-module.ts +38 -4
  322. package/src/presentation/routes/cancel-run.test.ts +1 -1
  323. package/src/presentation/routes/checkout-token.test.ts +357 -142
  324. package/src/presentation/routes/checkout-token.ts +56 -26
  325. package/src/presentation/routes/get-run-aggregates.test.ts +1 -1
  326. package/src/presentation/routes/get-run.test.ts +29 -1
  327. package/src/presentation/routes/get-step-secrets.test.ts +7 -2
  328. package/src/presentation/routes/leased-step.ts +9 -1
  329. package/src/presentation/routes/list-run-attempts.test.ts +1 -1
  330. package/src/presentation/routes/list-runs.test.ts +58 -1
  331. package/src/presentation/routes/list-runs.ts +8 -3
  332. package/src/presentation/routes/report-step.test.ts +25 -0
  333. package/src/presentation/routes/report-step.ts +10 -2
  334. package/src/presentation/routes/require-accessible-run.ts +4 -1
  335. package/src/presentation/routes/rerun-run.test.ts +1 -1
  336. package/src/presentation/subscribers/on-job-steps-settled.ts +1 -1
  337. package/src/presentation/subscribers/on-runner-job-claimed.test.ts +49 -0
  338. package/src/presentation/subscribers/on-runner-job-claimed.ts +22 -0
  339. package/src/temporal/activities/index.ts +1 -0
  340. package/src/temporal/activities/orchestration-activities.ts +1 -0
  341. package/src/temporal/constants.ts +1 -0
  342. package/src/temporal/index.ts +1 -0
  343. package/src/temporal/workflows/index.ts +1 -0
  344. package/src/temporal/workflows/job-execution-orchestration.test.ts +142 -6
  345. package/src/temporal/workflows/job-execution-orchestration.ts +99 -50
  346. package/src/temporal/workflows/run-orchestration.test.ts +1 -1
  347. package/src/temporal/workflows/test-env.ts +50 -13
  348. package/test/factories/project.ts +2 -0
  349. package/test/factories/workflow-model.ts +80 -26
  350. package/test/factories/workflow-run.ts +4 -1
  351. package/test/fixtures/agent-inter-module.ts +2 -1
  352. package/test/fixtures/job-with-steps.ts +1 -1
  353. package/tsconfig.build.tsbuildinfo +1 -1
  354. package/drizzle/0001_agent_tool_materialization.sql +0 -1
@@ -1,20 +1,23 @@
1
- import {requireLeasedJobContext} from '@shipfox/api-auth-context';
2
1
  import {
3
2
  type IntegrationsModuleClient,
4
3
  integrationsInterModuleContract,
5
4
  } from '@shipfox/api-integration-core-dto/inter-module';
6
- import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
5
+ import {
6
+ type ProjectsModuleClient,
7
+ projectsInterModuleContract,
8
+ } from '@shipfox/api-projects-dto/inter-module';
7
9
  import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';
8
- import {checkoutTokenResponseSchema} from '@shipfox/api-workflows-dto';
10
+ import {
11
+ checkoutTokenParamsSchema,
12
+ checkoutTokenQuerySchema,
13
+ checkoutTokenResponseSchema,
14
+ } from '@shipfox/api-workflows-dto';
9
15
  import {isInterModuleKnownError} from '@shipfox/inter-module';
10
16
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
11
- import {createJobCheckoutSpec} from '#core/checkout.js';
12
- import {
13
- CheckoutIntentUnresolvedError,
14
- JobNotFoundError,
15
- WorkflowRunNotFoundError,
16
- } from '#core/errors.js';
17
+ import {createStepCheckoutSpec} from '#core/checkout.js';
18
+ import {CheckoutConfigInvalidError, CheckoutIntentUnresolvedError} from '#core/errors.js';
17
19
  import {toCheckoutTokenDto} from '#presentation/dto/checkout-token.js';
20
+ import {loadRunningLeasedStep} from './leased-step.js';
18
21
 
19
22
  export function createCheckoutTokenRoute(clients: {
20
23
  runners: RunnersInterModuleClient;
@@ -23,10 +26,14 @@ export function createCheckoutTokenRoute(clients: {
23
26
  }) {
24
27
  return defineRoute({
25
28
  method: 'POST',
26
- path: '/checkout-token',
29
+ path: '/steps/:stepId/checkout-token',
27
30
  description:
28
- "Exchanges the runner's job lease for short-lived, read-only checkout credentials for the job's repository. The job is identified by the access token, so no job ID is needed. The checkout target is resolved server-side from the job's run and project; no credential material is stored on the job or run. Returns the repository URL, ref, and (when the provider needs auth) a short-lived credential that expires soon.",
29
- schema: {response: {200: checkoutTokenResponseSchema}},
31
+ "Exchanges the runner's lease for short-lived checkout credentials for the currently running checkout step. The step id and attempt are checked against the lease and the server-frozen step config supplies the target, ref, permissions, and fetch depth.",
32
+ schema: {
33
+ params: checkoutTokenParamsSchema,
34
+ querystring: checkoutTokenQuerySchema,
35
+ response: {200: checkoutTokenResponseSchema},
36
+ },
30
37
  errorHandler: (error) => {
31
38
  const known = isInterModuleKnownError(
32
39
  integrationsInterModuleContract.methods.createCheckoutSpec,
@@ -34,12 +41,24 @@ export function createCheckoutTokenRoute(clients: {
34
41
  )
35
42
  ? error
36
43
  : undefined;
37
- if (error instanceof JobNotFoundError)
38
- throw new ClientError(error.message, 'job-not-found', {status: 404});
39
- if (error instanceof WorkflowRunNotFoundError)
40
- throw new ClientError(error.message, 'run-not-found', {status: 404});
44
+ const targetError = isInterModuleKnownError(
45
+ projectsInterModuleContract.methods.resolveCheckoutTarget,
46
+ error,
47
+ )
48
+ ? error
49
+ : undefined;
41
50
  if (error instanceof CheckoutIntentUnresolvedError)
42
51
  throw new ClientError(error.message, 'checkout-unavailable', {status: 404});
52
+ if (error instanceof CheckoutConfigInvalidError)
53
+ throw new ClientError('Checkout configuration is invalid', 'checkout-config-invalid', {
54
+ status: 409,
55
+ });
56
+ if (targetError?.code === 'checkout-repository-not-authorized')
57
+ throw new ClientError(
58
+ 'Checkout repository is not authorized for this workspace',
59
+ 'checkout-repository-not-authorized',
60
+ {status: 404},
61
+ );
43
62
  if (known?.code === 'connection-not-found')
44
63
  throw new ClientError(
45
64
  'Integration connection not found',
@@ -101,21 +120,32 @@ export function createCheckoutTokenRoute(clients: {
101
120
  throw error;
102
121
  },
103
122
  handler: async (request, reply) => {
104
- const leasedJob = requireLeasedJobContext(request);
105
- const {active: leaseIsActive} = await clients.runners.getLeaseState({
106
- jobId: leasedJob.jobId,
107
- jobExecutionId: leasedJob.jobExecutionId,
108
- runnerSessionId: leasedJob.runnerSessionId,
123
+ const {stepId} = request.params;
124
+ const {attempt} = request.query;
125
+ const {step, workspaceId, projectId, triggerReference} = await loadRunningLeasedStep({
126
+ runners: clients.runners,
127
+ request,
128
+ stepId,
129
+ attempt,
109
130
  });
110
- if (!leaseIsActive)
111
- throw new ClientError('Job lease is no longer active', 'lease-not-active', {status: 404});
112
- const checkout = await createJobCheckoutSpec({
113
- jobId: leasedJob.jobId,
131
+
132
+ if (step.type !== 'setup' && step.type !== 'checkout') {
133
+ throw new ClientError('Step is not a checkout step', 'step-not-checkout', {status: 409});
134
+ }
135
+
136
+ const checkout = await createStepCheckoutSpec({
137
+ step,
138
+ workspaceId,
139
+ projectId,
140
+ triggerReference,
114
141
  integrations: clients.integrations,
115
142
  projects: clients.projects,
116
143
  });
117
144
  reply.header('cache-control', 'no-store');
118
- return toCheckoutTokenDto(checkout.spec, {persist: checkout.persistCredentials});
145
+ return toCheckoutTokenDto(checkout.spec, {
146
+ fetchDepth: checkout.fetchDepth,
147
+ persist: checkout.persistCredentials,
148
+ });
119
149
  },
120
150
  });
121
151
  }
@@ -39,7 +39,7 @@ describe('GET /api/workflows/runs/aggregates', () => {
39
39
  buildUserContext({
40
40
  userId: crypto.randomUUID(),
41
41
  email: 'user@example.com',
42
- memberships: [{workspaceId, role: 'admin'}],
42
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
43
43
  }),
44
44
  );
45
45
  done();
@@ -49,6 +49,7 @@ const projects = {
49
49
  describe('GET /api/workflows/runs/:id', () => {
50
50
  let app: FastifyInstance;
51
51
  let workspaceId: string;
52
+ let workspaceStatus: 'active' | 'deleted';
52
53
 
53
54
  beforeAll(async () => {
54
55
  app = Fastify();
@@ -60,7 +61,7 @@ describe('GET /api/workflows/runs/:id', () => {
60
61
  buildUserContext({
61
62
  userId: crypto.randomUUID(),
62
63
  email: 'user@example.com',
63
- memberships: [{workspaceId, role: 'admin'}],
64
+ memberships: [{workspaceId, role: 'admin', workspaceStatus}],
64
65
  }),
65
66
  );
66
67
  done();
@@ -71,6 +72,7 @@ describe('GET /api/workflows/runs/:id', () => {
71
72
 
72
73
  beforeEach(() => {
73
74
  workspaceId = crypto.randomUUID();
75
+ workspaceStatus = 'active';
74
76
  projectAccessState.workspaceId = workspaceId;
75
77
  getProjectById.mockImplementation(({projectId}) =>
76
78
  Promise.resolve({
@@ -115,6 +117,8 @@ describe('GET /api/workflows/runs/:id', () => {
115
117
  expect(res.statusCode).toBe(200);
116
118
  const body = res.json();
117
119
  expect(body.id).toBe(run.id);
120
+ expect(body).toMatchObject({name: 'Test', workflow_name: 'Test'});
121
+ expect(body.number).toBe(run.number);
118
122
  expect(body.source_snapshot).toBeNull();
119
123
  expect(body.jobs).toHaveLength(1);
120
124
  expect(body.jobs[0].key).toBe('build');
@@ -368,6 +372,30 @@ jobs:
368
372
  expect(detailSpy).not.toHaveBeenCalled();
369
373
  });
370
374
 
375
+ test('preserves workspace-inactive for a deleted membership claim', async () => {
376
+ workspaceStatus = 'deleted';
377
+ const run = await createWorkflowRun({
378
+ workspaceId,
379
+ projectId: crypto.randomUUID(),
380
+ definitionId: crypto.randomUUID(),
381
+ model: workflowModel({name: 'Test'}),
382
+ triggerPayload: {
383
+ source: 'manual',
384
+ event: 'fire',
385
+ subscriptionId: crypto.randomUUID(),
386
+ userId: crypto.randomUUID(),
387
+ },
388
+ });
389
+
390
+ const res = await app.inject({
391
+ method: 'GET',
392
+ url: `/api/workflows/runs/${run.id}`,
393
+ });
394
+
395
+ expect(res.statusCode).toBe(403);
396
+ expect(res.json().code).toBe('workspace-inactive');
397
+ });
398
+
371
399
  test('propagates unexpected errors from project access check', async () => {
372
400
  const run = await createWorkflowRun({
373
401
  workspaceId,
@@ -1,5 +1,6 @@
1
1
  import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
2
2
  import {createCapturingLogger} from '@shipfox/node-log/test';
3
+ import {agentThinkingSchema} from '@shipfox/workflow-document';
3
4
  import {eq} from 'drizzle-orm';
4
5
  import type {StepStatus} from '#core/entities/step.js';
5
6
  import {db} from '#db/db.js';
@@ -205,7 +206,11 @@ describe('GET /runs/jobs/current/steps/:stepId/secrets', () => {
205
206
 
206
207
  const scope = await getJobScope(job.id);
207
208
 
208
- expect(scope).toEqual({workspaceId: run.workspaceId, projectId: run.projectId});
209
+ expect(scope).toEqual({
210
+ workspaceId: run.workspaceId,
211
+ projectId: run.projectId,
212
+ triggerReference: null,
213
+ });
209
214
  });
210
215
  });
211
216
 
@@ -256,7 +261,7 @@ async function createStep(params: {
256
261
  harness: defaults.harness ?? 'pi',
257
262
  provider: defaults.provider ?? 'anthropic',
258
263
  model: defaults.model ?? 'claude-opus-4-8',
259
- thinking: defaults.thinking ?? 'high',
264
+ thinking: agentThinkingSchema.safeParse(defaults.thinking).data ?? 'high',
260
265
  }),
261
266
  triggerPayload: {
262
267
  source: 'manual',
@@ -2,6 +2,7 @@ import {type LeasedJobContext, requireLeasedJobContext} from '@shipfox/api-auth-
2
2
  import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';
3
3
  import {ClientError} from '@shipfox/node-fastify';
4
4
  import type {Step} from '#core/entities/step.js';
5
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
5
6
  import {getJobScope, getStepByIdForJobExecution} from '#db/index.js';
6
7
 
7
8
  export interface LoadedRunningLeasedStep {
@@ -9,6 +10,7 @@ export interface LoadedRunningLeasedStep {
9
10
  step: Step;
10
11
  workspaceId: string;
11
12
  projectId: string;
13
+ triggerReference: WorkflowRunTriggerReference | null;
12
14
  }
13
15
 
14
16
  export async function loadRunningLeasedStep(params: {
@@ -51,5 +53,11 @@ export async function loadRunningLeasedStep(params: {
51
53
  throw new ClientError('Step is not running', 'step-not-running', {status: 409});
52
54
  }
53
55
 
54
- return {leasedJob, step, workspaceId: scope.workspaceId, projectId: scope.projectId};
56
+ return {
57
+ leasedJob,
58
+ step,
59
+ workspaceId: scope.workspaceId,
60
+ projectId: scope.projectId,
61
+ triggerReference: scope.triggerReference,
62
+ };
55
63
  }
@@ -36,7 +36,7 @@ describe('GET /api/workflows/runs/:id/attempts', () => {
36
36
  buildUserContext({
37
37
  userId: crypto.randomUUID(),
38
38
  email: 'user@example.com',
39
- memberships: [{workspaceId, role: 'admin'}],
39
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
40
40
  }),
41
41
  );
42
42
  done();
@@ -43,7 +43,7 @@ describe('GET /api/workflows/runs', () => {
43
43
  buildUserContext({
44
44
  userId: crypto.randomUUID(),
45
45
  email: 'user@example.com',
46
- memberships: [{workspaceId, role: 'admin'}],
46
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
47
47
  }),
48
48
  );
49
49
  done();
@@ -96,6 +96,7 @@ describe('GET /api/workflows/runs', () => {
96
96
  expect(body.runs).toHaveLength(2);
97
97
  expect(body.runs[0].project_id).toBe(projectId);
98
98
  expect(body.runs[0].name).toBeDefined();
99
+ expect(body.runs[0].workflow_name).toBeDefined();
99
100
  expect(body.runs[0].trigger_source).toBe('manual');
100
101
  // The runs list carries run-level timing (null until the run starts).
101
102
  expect(body.runs[0]).toMatchObject({started_at: null, finished_at: null});
@@ -103,6 +104,62 @@ describe('GET /api/workflows/runs', () => {
103
104
  expect(body.filtered_total_count).toBe(2);
104
105
  });
105
106
 
107
+ test('carries the current attempt jobs in graph order so a row can draw its status strip', async () => {
108
+ await createWorkflowRun({
109
+ workspaceId,
110
+ projectId,
111
+ definitionId: crypto.randomUUID(),
112
+ name: 'Pipeline',
113
+ model: workflowModel({
114
+ name: 'Pipeline',
115
+ jobs: {
116
+ build: {steps: [{run: 'echo build'}]},
117
+ test: {needs: 'build', steps: [{run: 'echo test'}]},
118
+ deploy: {needs: 'test', steps: [{run: 'echo deploy'}]},
119
+ },
120
+ }),
121
+ triggerPayload: {
122
+ source: 'manual',
123
+ event: 'fire',
124
+ subscriptionId: crypto.randomUUID(),
125
+ userId: crypto.randomUUID(),
126
+ },
127
+ });
128
+
129
+ const res = await app.inject({
130
+ method: 'GET',
131
+ url: `/api/workflows/runs?project_id=${projectId}`,
132
+ });
133
+
134
+ expect(res.statusCode).toBe(200);
135
+ const [run] = res.json().runs;
136
+ expect(run.jobs.map((job: {key: string}) => job.key)).toEqual(['build', 'test', 'deploy']);
137
+ expect(run.jobs[0]).toMatchObject({status: 'pending', position: 0});
138
+ });
139
+
140
+ test('reports a null trigger reference for a run no source-control event produced', async () => {
141
+ await createWorkflowRun({
142
+ workspaceId,
143
+ projectId,
144
+ definitionId: crypto.randomUUID(),
145
+ name: 'Manual',
146
+ model: workflowModel({name: 'Manual'}),
147
+ triggerPayload: {
148
+ source: 'manual',
149
+ event: 'fire',
150
+ subscriptionId: crypto.randomUUID(),
151
+ userId: crypto.randomUUID(),
152
+ },
153
+ });
154
+
155
+ const res = await app.inject({
156
+ method: 'GET',
157
+ url: `/api/workflows/runs?project_id=${projectId}`,
158
+ });
159
+
160
+ expect(res.json().runs[0].trigger_reference).toBeNull();
161
+ });
162
+
106
163
  test('returns empty array for project with no runs', async () => {
107
164
  const res = await app.inject({
108
165
  method: 'GET',
@@ -6,8 +6,8 @@ import {
6
6
  import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
7
7
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
8
8
  import {logger} from '@shipfox/node-opentelemetry';
9
- import {listWorkflowRuns} from '#db/index.js';
10
- import {toRunDto} from '#presentation/dto/index.js';
9
+ import {listWorkflowRunJobSummaries, listWorkflowRuns} from '#db/index.js';
10
+ import {toRunListItemDto} from '#presentation/dto/index.js';
11
11
  import {requireProjectAccess} from './project-access.js';
12
12
 
13
13
  export function listRunsRoute(projects: ProjectsModuleClient) {
@@ -54,6 +54,11 @@ export function listRunsRoute(projects: ProjectsModuleClient) {
54
54
  filters,
55
55
  includeTotal: !decodedCursor,
56
56
  });
57
+ // Paired with the attempt this read returned, so a re-run landing mid-request cannot
58
+ // pair one attempt's metadata with another's jobs.
59
+ const jobsByRun = await listWorkflowRunJobSummaries(
60
+ result.runs.map((run) => ({id: run.id, currentAttempt: run.currentAttempt})),
61
+ );
57
62
 
58
63
  logger().info(
59
64
  {
@@ -71,7 +76,7 @@ export function listRunsRoute(projects: ProjectsModuleClient) {
71
76
  );
72
77
 
73
78
  return {
74
- runs: result.runs.map((run) => toRunDto(run)),
79
+ runs: result.runs.map((run) => toRunListItemDto(run, jobsByRun.get(run.id))),
75
80
  next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,
76
81
  filtered_total_count: result.filteredTotalCount,
77
82
  };
@@ -215,6 +215,31 @@ describe('POST /runs/jobs/current/steps/:stepId/report', () => {
215
215
  expect(attempt?.exitCode).toBe(0);
216
216
  });
217
217
 
218
+ test('persists dedicated checkout details alongside user output', async () => {
219
+ const {jobId, steps} = await arrangeJobWithSteps(1);
220
+ const token = await mintActiveLeaseToken({jobId});
221
+ await nextStepForJob(jobId);
222
+ const checkout = {
223
+ repository: 'https://github.com/acme/api.git',
224
+ ref: 'refs/pull/412/head',
225
+ commit: '9f2c000000000000000000000000000000000000',
226
+ path: '/runner/workspace/job-1',
227
+ };
228
+ const res = await app.inject({
229
+ method: 'POST',
230
+ url: reportUrl(steps[0]?.id as string),
231
+ headers: {authorization: `Bearer ${token}`},
232
+ payload: reportPayload({
233
+ status: 'succeeded',
234
+ output: {artifact: 'dist/app.tgz', checkout: 'user-value'},
235
+ checkout,
236
+ exit_code: 0,
237
+ }),
238
+ });
239
+ expect(res.statusCode).toBe(200);
240
+ const [attempt] = await getStepAttempts(jobId);
241
+ expect(attempt?.output).toEqual({artifact: 'dist/app.tgz', checkout});
242
+ });
218
243
  test('rejects a failed report without an error', async () => {
219
244
  const {jobId, steps} = await arrangeJobWithSteps(1);
220
245
  const token = await mintActiveLeaseToken({jobId});
@@ -12,7 +12,7 @@ export function createReportStepRoute(runners: RunnersInterModuleClient) {
12
12
  method: 'POST',
13
13
  path: '/steps/:stepId/report',
14
14
  description:
15
- 'Reports whether a step succeeded or failed. The job is identified by the access token. Reporting the same step more than once is safe: once a step has finished, later reports for it are ignored. The runner does not call a separate job-completion endpoint finalization is driven server-side from the recorded step results, and the `cancel` flag tells the runner to stop once the job has finished without full success.',
15
+ 'Reports whether a step succeeded or failed. The job is identified by the access token. Reporting the same step more than once is safe: once a step has finished, later reports for it are ignored. The runner does not call a separate job-completion endpoint: finalization is driven server-side from the recorded step results, and the `cancel` flag tells the runner to stop once the job has finished without full success.',
16
16
  schema: {
17
17
  params: z.object({stepId: z.string().uuid()}),
18
18
  body: reportStepBodySchema,
@@ -45,12 +45,20 @@ export function createReportStepRoute(runners: RunnersInterModuleClient) {
45
45
  throw new ClientError('Job lease is no longer active', 'lease-not-active', {status: 404});
46
46
  }
47
47
 
48
+ // Checkout is a reserved system field on the report wire contract. Keep
49
+ // it out of the runner's user output merge until this persistence
50
+ // boundary, where the system value has explicit precedence.
51
+ const output =
52
+ request.body.checkout === undefined
53
+ ? (request.body.output ?? null)
54
+ : {...(request.body.output ?? {}), checkout: request.body.checkout};
55
+
48
56
  const outcome = await recordStepResult({
49
57
  jobExecutionId: leasedJob.jobExecutionId,
50
58
  stepId,
51
59
  status: request.body.status,
52
60
  error: fromStepErrorDto(request.body.error),
53
- output: request.body.output ?? null,
61
+ output,
54
62
  response: request.body.response ?? null,
55
63
  exitCode: request.body.exit_code ?? request.body.error?.exit_code ?? null,
56
64
  logOutcome: request.body.log_outcome,
@@ -20,7 +20,10 @@ export async function requireAccessibleRun({
20
20
  }
21
21
 
22
22
  await requireProjectAccess(request, run.projectId, projects).catch((err: unknown) => {
23
- if (err instanceof ClientError && (err.status === 403 || err.status === 404)) {
23
+ if (
24
+ err instanceof ClientError &&
25
+ (err.status === 404 || (err.status === 403 && err.code === 'forbidden'))
26
+ ) {
24
27
  throw new ClientError('Run not found', 'not-found', {status: 404});
25
28
  }
26
29
  throw err;
@@ -43,7 +43,7 @@ describe('POST /api/workflows/runs/:id/rerun', () => {
43
43
  buildUserContext({
44
44
  userId: crypto.randomUUID(),
45
45
  email: 'user@example.com',
46
- memberships: [{workspaceId, role: 'admin'}],
46
+ memberships: [{workspaceId, role: 'admin', workspaceStatus: 'active'}],
47
47
  }),
48
48
  );
49
49
  done();
@@ -7,7 +7,7 @@ import {isWorkflowNotFound} from '#temporal/workflow-not-found.js';
7
7
  // Per-step execution finishes a job inside recordStepResult (no runner /complete
8
8
  // call), which enqueues WORKFLOWS_JOB_STEPS_SETTLED in the same transaction. The
9
9
  // persisted per-step projection is already terminal, so the workflow only flips
10
- // the job status no steps are carried.
10
+ // the job status: no steps are carried.
11
11
  export async function onJobStepsSettled(payload: WorkflowsJobStepsSettledEventDto): Promise<void> {
12
12
  logger().info(
13
13
  {
@@ -2,7 +2,20 @@ import {getJobExecutionsByJobId, getJobsByWorkflowRunId} from '#db/index.js';
2
2
  import {workflowRunFactory} from '#test/index.js';
3
3
  import {onRunnerJobClaimed} from './on-runner-job-claimed.js';
4
4
 
5
+ const signalMock = vi.fn();
6
+ const getHandleMock = vi.fn(() => ({signal: signalMock}));
7
+
8
+ vi.mock('@shipfox/node-temporal', () => ({
9
+ temporalClient: () => ({workflow: {getHandle: getHandleMock}}),
10
+ }));
11
+
5
12
  describe('onRunnerJobClaimed', () => {
13
+ beforeEach(() => {
14
+ getHandleMock.mockClear();
15
+ signalMock.mockReset();
16
+ signalMock.mockResolvedValue(undefined);
17
+ });
18
+
6
19
  it('stamps started_at on the job execution from the claim event payload', async () => {
7
20
  const run = await workflowRunFactory.create();
8
21
  const job = (await getJobsByWorkflowRunId(run.id))[0];
@@ -23,6 +36,11 @@ describe('onRunnerJobClaimed', () => {
23
36
 
24
37
  const after = (await getJobExecutionsByJobId(job.id))[0];
25
38
  expect(after?.startedAt?.getTime()).toBe(claimedAt.getTime());
39
+ expect(getHandleMock).toHaveBeenCalledWith(`job:${job.id}`);
40
+ expect(signalMock).toHaveBeenCalledWith('job-claimed', {
41
+ jobExecutionId: jobExecution.id,
42
+ claimedAt: claimedAt.toISOString(),
43
+ });
26
44
  });
27
45
 
28
46
  it('is idempotent: a redelivered event keeps the first started_at (coalesce)', async () => {
@@ -54,4 +72,35 @@ describe('onRunnerJobClaimed', () => {
54
72
  const after = (await getJobExecutionsByJobId(job.id))[0];
55
73
  expect(after?.startedAt?.getTime()).toBe(first.getTime());
56
74
  });
75
+
76
+ it('discards a claim signal when the job workflow already terminated', async () => {
77
+ const notFound = new Error('gone');
78
+ notFound.name = 'WorkflowNotFoundError';
79
+ signalMock.mockRejectedValueOnce(notFound);
80
+
81
+ await expect(
82
+ onRunnerJobClaimed({
83
+ workflowRunId: crypto.randomUUID(),
84
+ workflowRunAttemptId: crypto.randomUUID(),
85
+ jobId: crypto.randomUUID(),
86
+ jobExecutionId: crypto.randomUUID(),
87
+ claimedAt: '2026-06-22T10:05:00.000Z',
88
+ }),
89
+ ).resolves.toBeUndefined();
90
+ });
91
+
92
+ it('rethrows a transient Temporal signal failure for subscriber retry', async () => {
93
+ const failure = new Error('temporal unavailable');
94
+ signalMock.mockRejectedValueOnce(failure);
95
+
96
+ await expect(
97
+ onRunnerJobClaimed({
98
+ workflowRunId: crypto.randomUUID(),
99
+ workflowRunAttemptId: crypto.randomUUID(),
100
+ jobId: crypto.randomUUID(),
101
+ jobExecutionId: crypto.randomUUID(),
102
+ claimedAt: '2026-06-22T10:05:00.000Z',
103
+ }),
104
+ ).rejects.toBe(failure);
105
+ });
57
106
  });
@@ -1,6 +1,9 @@
1
1
  import type {RunnerJobClaimedEvent} from '@shipfox/api-runners-dto';
2
2
  import {logger} from '@shipfox/node-opentelemetry';
3
+ import {temporalClient} from '@shipfox/node-temporal';
3
4
  import {recordJobExecutionStartedAt} from '#db/index.js';
5
+ import {JOB_CLAIMED_SIGNAL} from '#temporal/constants.js';
6
+ import {isWorkflowNotFound} from '#temporal/workflow-not-found.js';
4
7
 
5
8
  // Anticorruption layer: the runner reports a `claimed` fact in its own lease-broker
6
9
  // language; the run lifecycle treats the claim as the job's start, so we project it onto
@@ -20,4 +23,23 @@ export async function onRunnerJobClaimed(payload: RunnerJobClaimedEvent): Promis
20
23
  jobExecutionId: payload.jobExecutionId,
21
24
  startedAt: new Date(payload.claimedAt),
22
25
  });
26
+
27
+ const handle = temporalClient().workflow.getHandle(`job:${payload.jobId}`);
28
+ try {
29
+ await handle.signal(JOB_CLAIMED_SIGNAL, {
30
+ jobExecutionId: payload.jobExecutionId,
31
+ claimedAt: payload.claimedAt,
32
+ });
33
+ } catch (err) {
34
+ // A terminal execution can race the claim outbox delivery. Its persisted status is
35
+ // authoritative, so discard the signal if Temporal has already closed the workflow.
36
+ if (isWorkflowNotFound(err)) {
37
+ logger().debug(
38
+ {jobId: payload.jobId, jobExecutionId: payload.jobExecutionId},
39
+ 'Job workflow already terminated; claim event discarded',
40
+ );
41
+ return;
42
+ }
43
+ throw err;
44
+ }
23
45
  }
@@ -51,6 +51,7 @@ export function createOrchestrationActivities(params: {
51
51
  agent: params.agent,
52
52
  integrations: params.integrations,
53
53
  projects: params.projects,
54
+ secrets: params.secrets,
54
55
  }),
55
56
  peekListenerBufferActivity,
56
57
  resolveJobListenerActivity,
@@ -287,6 +287,7 @@ export function createDrainListenerEventsActivity(clients: {
287
287
  agent: AgentInterModuleClient;
288
288
  integrations?: IntegrationsModuleClient | undefined;
289
289
  projects?: ProjectsModuleClient | undefined;
290
+ secrets: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'>;
290
291
  }) {
291
292
  return async function drainListenerEventsActivity(params: {
292
293
  jobId: string;
@@ -2,5 +2,6 @@ export const WORKFLOWS_TASK_QUEUE = 'workflows-orchestrator';
2
2
  export const RUN_CANCEL_SIGNAL = 'run-cancel';
3
3
  export const JOB_FINISHED_SIGNAL = 'job-finished';
4
4
  export const JOB_LEASE_EXPIRED_SIGNAL = 'job-lease-expired';
5
+ export const JOB_CLAIMED_SIGNAL = 'job-claimed';
5
6
  export const LISTENER_EVENTS_AVAILABLE_SIGNAL = 'events-available';
6
7
  export const LISTENER_RESOLVE_SIGNAL = 'resolve';
@@ -1,5 +1,6 @@
1
1
  export {createOrchestrationActivities} from './activities/index.js';
2
2
  export {
3
+ JOB_CLAIMED_SIGNAL,
3
4
  JOB_FINISHED_SIGNAL,
4
5
  JOB_LEASE_EXPIRED_SIGNAL,
5
6
  WORKFLOWS_TASK_QUEUE,
@@ -1,4 +1,5 @@
1
1
  export {
2
+ jobClaimedSignal,
2
3
  jobExecutionOrchestration,
3
4
  jobFinishedSignal,
4
5
  jobLeaseExpiredSignal,