@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,19 +1,43 @@
1
- import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto/inter-module';
2
- import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
1
+ import type {WorkflowModel} from '@shipfox/api-definitions-dto';
2
+ import {
3
+ type IntegrationsModuleClient,
4
+ integrationsInterModuleContract,
5
+ } from '@shipfox/api-integration-core-dto/inter-module';
6
+ import {
7
+ type ProjectsModuleClient,
8
+ projectsInterModuleContract,
9
+ } from '@shipfox/api-projects-dto/inter-module';
3
10
  import {createInterModuleKnownError} from '@shipfox/inter-module';
4
11
  import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
5
12
  import {createCapturingLogger} from '@shipfox/node-log/test';
6
- import {jobFactory} from '#test/factories/job.js';
13
+ import {eq} from 'drizzle-orm';
14
+ import type {StepStatus} from '#core/entities/step.js';
15
+ import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
16
+ import {db} from '#db/db.js';
17
+ import {jobs as jobsTable} from '#db/schema/jobs.js';
18
+ import {steps as stepsTable} from '#db/schema/steps.js';
19
+ import {workflowRuns} from '#db/schema/workflow-runs.js';
20
+ import {createWorkflowRun, getJobsByWorkflowRunId, getStepsByJobId} from '#db/workflow-runs.js';
7
21
  import {projectFactory} from '#test/factories/project.js';
8
- import {mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
22
+ import {workflowModel} from '#test/factories/workflow-model.js';
23
+ import {insertRunningJobLease, mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
9
24
  import {fakeLeaseTokenAuthMethod, mintLeaseToken} from '#test/fixtures/lease-token.js';
10
25
  import {runnersTestClient} from '#test/fixtures/runners-inter-module.js';
11
26
  import {createLeaseTokenRouteGroup} from './index.js';
12
27
 
13
28
  const getProjectById = vi.fn();
14
- const projects = {getProjectById} as Pick<ProjectsModuleClient, 'getProjectById'>;
29
+ const resolveCheckoutTarget = vi.fn();
30
+ const projects = {
31
+ getProjectById,
32
+ resolveCheckoutTarget,
33
+ } as Pick<ProjectsModuleClient, 'getProjectById' | 'resolveCheckoutTarget'>;
15
34
 
16
- const URL = '/runs/jobs/current/checkout-token';
35
+ const createCheckoutSpec = vi.fn();
36
+ const integrations = {
37
+ createCheckoutSpec,
38
+ } as Pick<IntegrationsModuleClient, 'createCheckoutSpec'>;
39
+
40
+ const {logger, lines: logLines, clear: clearLogLines} = createCapturingLogger();
17
41
 
18
42
  const githubSpec = (token: string) => ({
19
43
  repositoryUrl: 'https://github.com/acme/repo.git',
@@ -21,10 +45,8 @@ const githubSpec = (token: string) => ({
21
45
  credentials: {username: 'x-access-token', token, expiresAt: new Date('2026-06-10T12:00:00.000Z')},
22
46
  });
23
47
 
24
- describe('POST /runs/jobs/current/checkout-token', () => {
48
+ describe('POST /runs/jobs/current/steps/:stepId/checkout-token', () => {
25
49
  let app: FastifyInstance;
26
- const createCheckoutSpec = vi.fn();
27
- const {logger, lines: logLines, clear: clearLogLines} = createCapturingLogger();
28
50
 
29
51
  beforeAll(async () => {
30
52
  app = await createApp({
@@ -34,7 +56,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
34
56
  agent: {} as never,
35
57
  annotations: {} as never,
36
58
  auth: {} as never,
37
- integrations: {createCheckoutSpec} as never,
59
+ integrations: integrations as never,
38
60
  projects: projects as never,
39
61
  runners: runnersTestClient,
40
62
  secrets: {} as never,
@@ -49,6 +71,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
49
71
  beforeEach(() => {
50
72
  createCheckoutSpec.mockReset();
51
73
  getProjectById.mockReset();
74
+ resolveCheckoutTarget.mockReset();
52
75
  clearLogLines();
53
76
  });
54
77
 
@@ -56,25 +79,39 @@ describe('POST /runs/jobs/current/checkout-token', () => {
56
79
  await closeApp();
57
80
  });
58
81
 
59
- describe('lease-token auth', () => {
60
- test('rejects a request without an Authorization header', async () => {
61
- const res = await app.inject({method: 'POST', url: URL});
82
+ test('rejects a request without an Authorization header', async () => {
83
+ const res = await app.inject({
84
+ method: 'POST',
85
+ url: checkoutUrl(crypto.randomUUID(), 1),
86
+ });
87
+
88
+ expect(res.statusCode).toBe(401);
89
+ expect(res.json().code).toBe('unauthorized');
90
+ });
62
91
 
63
- expect(res.statusCode).toBe(401);
64
- expect(res.json().code).toBe('unauthorized');
92
+ test('does not keep the old job-scoped route as a compatibility alias', async () => {
93
+ const res = await app.inject({
94
+ method: 'POST',
95
+ url: '/runs/jobs/current/checkout-token',
65
96
  });
97
+
98
+ expect(res.statusCode).toBe(404);
66
99
  });
67
100
 
68
- test('returns basic auth for a GitHub-style spec with credentials', async () => {
69
- const project = projectFactory.build();
101
+ test('mints against the frozen setup-step config and returns its default fetch depth', async () => {
102
+ const {project, job, step} = await createRunningCheckoutStep();
70
103
  getProjectById.mockResolvedValue({project});
71
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
104
+ resolveCheckoutTarget.mockResolvedValue({
105
+ projectId: project.id,
106
+ connectionId: project.sourceConnectionId,
107
+ externalRepositoryId: project.sourceExternalRepositoryId,
108
+ });
72
109
  createCheckoutSpec.mockResolvedValue(githubSpec('ghs-secret-token'));
73
110
  const token = await mintActiveLeaseToken({jobId: job.id});
74
111
 
75
112
  const res = await app.inject({
76
113
  method: 'POST',
77
- url: URL,
114
+ url: checkoutUrl(step.id, step.currentAttempt),
78
115
  headers: {authorization: `Bearer ${token}`},
79
116
  });
80
117
 
@@ -83,6 +120,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
83
120
  expect(res.json()).toEqual({
84
121
  repository_url: 'https://github.com/acme/repo.git',
85
122
  ref: 'main',
123
+ fetch_depth: 1,
86
124
  auth: {
87
125
  kind: 'basic',
88
126
  username: 'x-access-token',
@@ -93,255 +131,342 @@ describe('POST /runs/jobs/current/checkout-token', () => {
93
131
  persist: true,
94
132
  },
95
133
  });
134
+ expect(resolveCheckoutTarget).toHaveBeenCalledWith({
135
+ workspaceId: project.workspaceId,
136
+ defaults: {connectionId: project.sourceConnectionId, owner: 'acme'},
137
+ target: {project: project.id},
138
+ });
139
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
140
+ workspaceId: project.workspaceId,
141
+ connectionId: project.sourceConnectionId,
142
+ externalRepositoryId: project.sourceExternalRepositoryId,
143
+ permissions: {contents: 'read'},
144
+ });
96
145
  });
97
146
 
98
- test('omits auth for a credential-free (debug) spec', async () => {
99
- const project = projectFactory.build();
147
+ test('defaults the checkout ref to the run trigger commit for the same project', async () => {
148
+ const {run, project, job, step} = await createRunningCheckoutStep();
149
+ const triggerReference = {
150
+ project: {id: project.id},
151
+ repository: 'acme/repo',
152
+ ref: 'refs/heads/feature/checkout',
153
+ commit: 'a'.repeat(40),
154
+ actor: null,
155
+ } satisfies WorkflowRunTriggerReference;
156
+ await db().update(workflowRuns).set({triggerReference}).where(eq(workflowRuns.id, run.id));
100
157
  getProjectById.mockResolvedValue({project});
101
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
102
- createCheckoutSpec.mockResolvedValue({
103
- repositoryUrl: 'https://example.com/acme/repo.git',
104
- ref: 'trunk',
158
+ resolveCheckoutTarget.mockResolvedValue({
159
+ projectId: project.id,
160
+ connectionId: project.sourceConnectionId,
161
+ externalRepositoryId: project.sourceExternalRepositoryId,
105
162
  });
163
+ createCheckoutSpec.mockResolvedValue(githubSpec('ghs-trigger-token'));
106
164
  const token = await mintActiveLeaseToken({jobId: job.id});
107
165
 
108
166
  const res = await app.inject({
109
167
  method: 'POST',
110
- url: URL,
168
+ url: checkoutUrl(step.id, step.currentAttempt),
111
169
  headers: {authorization: `Bearer ${token}`},
112
170
  });
113
171
 
114
172
  expect(res.statusCode).toBe(200);
115
- expect(res.json()).toEqual({
116
- repository_url: 'https://example.com/acme/repo.git',
117
- ref: 'trunk',
173
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
174
+ workspaceId: project.workspaceId,
175
+ connectionId: project.sourceConnectionId,
176
+ externalRepositoryId: project.sourceExternalRepositoryId,
177
+ ref: triggerReference.commit,
178
+ permissions: {contents: 'read'},
118
179
  });
119
180
  });
120
181
 
121
- test('returns 404 for a token without an active lease', async () => {
122
- const token = await mintLeaseToken({
123
- jobId: crypto.randomUUID(),
124
- jobExecutionId: crypto.randomUUID(),
125
- });
182
+ test('returns checkout-unavailable when the run project no longer resolves', async () => {
183
+ const {job, step} = await createRunningCheckoutStep({kind: 'checkout'});
184
+ getProjectById.mockResolvedValue({project: null});
185
+ const token = await mintActiveLeaseToken({jobId: job.id});
126
186
 
127
187
  const res = await app.inject({
128
188
  method: 'POST',
129
- url: URL,
189
+ url: checkoutUrl(step.id, step.currentAttempt),
130
190
  headers: {authorization: `Bearer ${token}`},
131
191
  });
132
192
 
133
193
  expect(res.statusCode).toBe(404);
134
- expect(res.json().code).toBe('lease-not-active');
194
+ expect(res.json().code).toBe('checkout-unavailable');
195
+ expect(resolveCheckoutTarget).not.toHaveBeenCalled();
135
196
  expect(createCheckoutSpec).not.toHaveBeenCalled();
136
197
  });
137
198
 
138
- test.each([
139
- 'succeeded',
140
- 'failed',
141
- 'cancelled',
142
- 'skipped',
143
- ] as const)('returns 404 and mints nothing without an active lease for %s job', async (status) => {
144
- const project = projectFactory.build();
145
- getProjectById.mockResolvedValue({project});
146
- const job = await jobFactory.create({}, {transient: {projectId: project.id, status}});
147
- const token = await mintLeaseToken({jobId: job.id});
199
+ test('returns a client error for a malformed project target', async () => {
200
+ const {job, step} = await createRunningCheckoutStep({
201
+ kind: 'checkout',
202
+ checkout: {project: 'not-a-uuid'},
203
+ });
204
+ const token = await mintActiveLeaseToken({jobId: job.id});
148
205
 
149
206
  const res = await app.inject({
150
207
  method: 'POST',
151
- url: URL,
208
+ url: checkoutUrl(step.id, step.currentAttempt),
152
209
  headers: {authorization: `Bearer ${token}`},
153
210
  });
154
211
 
155
- expect(res.statusCode).toBe(404);
156
- expect(res.json().code).toBe('lease-not-active');
212
+ expect(res.statusCode).toBe(409);
213
+ expect(res.json().code).toBe('checkout-config-invalid');
214
+ expect(getProjectById).not.toHaveBeenCalled();
215
+ expect(resolveCheckoutTarget).not.toHaveBeenCalled();
157
216
  expect(createCheckoutSpec).not.toHaveBeenCalled();
158
217
  });
159
218
 
160
- test('returns checkout credentials while the parent job projection is still pending', async () => {
161
- const project = projectFactory.build();
219
+ test('omits auth for a credential-free checkout spec', async () => {
220
+ const {project, job, step} = await createRunningCheckoutStep();
162
221
  getProjectById.mockResolvedValue({project});
163
- const job = await jobFactory.create(
164
- {},
165
- {transient: {projectId: project.id, status: 'pending'}},
166
- );
167
- createCheckoutSpec.mockResolvedValue(githubSpec('token'));
222
+ resolveCheckoutTarget.mockResolvedValue({
223
+ projectId: project.id,
224
+ connectionId: project.sourceConnectionId,
225
+ externalRepositoryId: project.sourceExternalRepositoryId,
226
+ });
227
+ createCheckoutSpec.mockResolvedValue({
228
+ repositoryUrl: 'https://example.com/acme/repo.git',
229
+ ref: 'trunk',
230
+ });
168
231
  const token = await mintActiveLeaseToken({jobId: job.id});
169
232
 
170
233
  const res = await app.inject({
171
234
  method: 'POST',
172
- url: URL,
235
+ url: checkoutUrl(step.id, step.currentAttempt),
173
236
  headers: {authorization: `Bearer ${token}`},
174
237
  });
175
238
 
176
239
  expect(res.statusCode).toBe(200);
177
- expect(createCheckoutSpec).toHaveBeenCalledWith(
178
- expect.objectContaining({connectionId: project.sourceConnectionId}),
179
- );
240
+ expect(res.json()).toEqual({
241
+ repository_url: 'https://example.com/acme/repo.git',
242
+ ref: 'trunk',
243
+ fetch_depth: 1,
244
+ });
180
245
  });
181
246
 
182
- test('ignores a hostile workflowRunAttemptId claim and resolves via the job row', async () => {
183
- const projectA = projectFactory.build();
184
- const projectB = projectFactory.build();
185
- const projectsById = new Map([
186
- [projectA.id, projectA],
187
- [projectB.id, projectB],
188
- ]);
189
- getProjectById.mockImplementation((input: {projectId: string}) =>
190
- Promise.resolve({project: projectsById.get(input.projectId) ?? null}),
191
- );
192
- const jobA = await jobFactory.create({}, {transient: {projectId: projectA.id}});
193
- const jobB = await jobFactory.create({}, {transient: {projectId: projectB.id}});
194
- createCheckoutSpec.mockResolvedValue(githubSpec('token'));
195
- const token = await mintActiveLeaseToken({
196
- jobId: jobA.id,
197
- token: {workflowRunAttemptId: jobB.workflowRunAttemptId},
247
+ test('mints an explicit checkout step from its target, ref, permissions, and fetch depth', async () => {
248
+ const targetProjectId = crypto.randomUUID();
249
+ const {project, job, step} = await createRunningCheckoutStep({
250
+ kind: 'checkout',
251
+ checkout: {
252
+ project: targetProjectId,
253
+ ref: 'refs/pull/412/head',
254
+ fetchDepth: 0,
255
+ permissions: {contents: 'write'},
256
+ persistCredentials: false,
257
+ },
198
258
  });
259
+ getProjectById.mockResolvedValue({project});
260
+ resolveCheckoutTarget.mockResolvedValue({
261
+ projectId: targetProjectId,
262
+ connectionId: crypto.randomUUID(),
263
+ externalRepositoryId: 'github:412',
264
+ });
265
+ createCheckoutSpec.mockResolvedValue({
266
+ ...githubSpec('ghs-target-token'),
267
+ ref: 'refs/pull/412/head',
268
+ });
269
+ const token = await mintActiveLeaseToken({jobId: job.id});
199
270
 
200
271
  const res = await app.inject({
201
272
  method: 'POST',
202
- url: URL,
273
+ url: checkoutUrl(step.id, step.currentAttempt),
203
274
  headers: {authorization: `Bearer ${token}`},
204
275
  });
205
276
 
206
277
  expect(res.statusCode).toBe(200);
207
- expect(createCheckoutSpec).toHaveBeenCalledWith(
208
- expect.objectContaining({connectionId: projectA.sourceConnectionId}),
209
- );
210
- expect(createCheckoutSpec).not.toHaveBeenCalledWith(
211
- expect.objectContaining({connectionId: projectB.sourceConnectionId}),
278
+ expect(res.json()).toMatchObject({ref: 'refs/pull/412/head', fetch_depth: 0});
279
+ expect(res.json().auth.persist).toBe(false);
280
+ expect(resolveCheckoutTarget).toHaveBeenCalledWith(
281
+ expect.objectContaining({target: {project: targetProjectId}}),
212
282
  );
283
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
284
+ workspaceId: project.workspaceId,
285
+ connectionId: expect.any(String),
286
+ externalRepositoryId: 'github:412',
287
+ ref: 'refs/pull/412/head',
288
+ permissions: {contents: 'write'},
289
+ });
213
290
  });
214
291
 
215
- test('passes the project workspace, never the lease workspace claim', async () => {
216
- const project = projectFactory.build();
292
+ test('uses the job scope instead of a hostile lease workspace claim', async () => {
293
+ const {project, job, step} = await createRunningCheckoutStep();
217
294
  getProjectById.mockResolvedValue({project});
218
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
295
+ resolveCheckoutTarget.mockResolvedValue({
296
+ projectId: project.id,
297
+ connectionId: project.sourceConnectionId,
298
+ externalRepositoryId: project.sourceExternalRepositoryId,
299
+ });
219
300
  createCheckoutSpec.mockResolvedValue(githubSpec('token'));
220
301
  const token = await mintActiveLeaseToken({
221
302
  jobId: job.id,
222
303
  token: {workspaceId: crypto.randomUUID()},
223
304
  });
224
305
 
225
- await app.inject({method: 'POST', url: URL, headers: {authorization: `Bearer ${token}`}});
306
+ const res = await app.inject({
307
+ method: 'POST',
308
+ url: checkoutUrl(step.id, step.currentAttempt),
309
+ headers: {authorization: `Bearer ${token}`},
310
+ });
226
311
 
227
- expect(createCheckoutSpec).toHaveBeenCalledWith(
312
+ expect(res.statusCode).toBe(200);
313
+ expect(resolveCheckoutTarget).toHaveBeenCalledWith(
228
314
  expect.objectContaining({workspaceId: project.workspaceId}),
229
315
  );
230
316
  });
231
317
 
232
- test('returns 404 when the run has no project linkage', async () => {
233
- getProjectById.mockResolvedValue({project: null});
234
- const project = {id: crypto.randomUUID(), workspaceId: crypto.randomUUID()};
235
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
236
- const token = await mintActiveLeaseToken({jobId: job.id});
318
+ test('returns 404 and mints nothing when the lease is inactive', async () => {
319
+ const {job, step} = await createRunningCheckoutStep();
320
+ const token = await mintLeaseToken({jobId: job.id});
237
321
 
238
322
  const res = await app.inject({
239
323
  method: 'POST',
240
- url: URL,
324
+ url: checkoutUrl(step.id, step.currentAttempt),
241
325
  headers: {authorization: `Bearer ${token}`},
242
326
  });
243
327
 
244
328
  expect(res.statusCode).toBe(404);
245
- expect(res.json().code).toBe('checkout-unavailable');
329
+ expect(res.json().code).toBe('lease-not-active');
330
+ expect(resolveCheckoutTarget).not.toHaveBeenCalled();
331
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
246
332
  });
247
333
 
248
- test('maps a rate-limited provider error to 429 with retry_after_seconds', async () => {
249
- const project = projectFactory.build();
250
- getProjectById.mockResolvedValue({project});
251
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
252
- createCheckoutSpec.mockRejectedValue(
253
- createInterModuleKnownError(
254
- integrationsInterModuleContract.methods.createCheckoutSpec,
255
- 'provider-failure',
256
- {reason: 'rate-limited', retryAfterSeconds: 60},
257
- ),
258
- );
334
+ test('returns 404 when the requested step belongs to another job', async () => {
335
+ const first = await createRunningCheckoutStep();
336
+ const second = await createRunningCheckoutStep();
337
+ const token = await mintActiveLeaseToken({jobId: first.job.id});
338
+
339
+ const res = await app.inject({
340
+ method: 'POST',
341
+ url: checkoutUrl(second.step.id, second.step.currentAttempt),
342
+ headers: {authorization: `Bearer ${token}`},
343
+ });
344
+
345
+ expect(res.statusCode).toBe(404);
346
+ expect(res.json().code).toBe('step-not-found');
347
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
348
+ });
349
+
350
+ test('returns 409 for a stale step attempt', async () => {
351
+ const {job, step} = await createRunningCheckoutStep();
259
352
  const token = await mintActiveLeaseToken({jobId: job.id});
260
353
 
261
354
  const res = await app.inject({
262
355
  method: 'POST',
263
- url: URL,
356
+ url: checkoutUrl(step.id, step.currentAttempt + 1),
264
357
  headers: {authorization: `Bearer ${token}`},
265
358
  });
266
359
 
267
- expect(res.statusCode).toBe(429);
268
- expect(res.json().code).toBe('rate-limited');
269
- expect(res.json().details.retry_after_seconds).toBe(60);
360
+ expect(res.statusCode).toBe(409);
361
+ expect(res.json().code).toBe('step-attempt-mismatch');
362
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
270
363
  });
271
364
 
272
- test('maps an inactive connection error to 422', async () => {
273
- const project = projectFactory.build();
274
- getProjectById.mockResolvedValue({project});
275
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
276
- createCheckoutSpec.mockRejectedValue(
277
- createInterModuleKnownError(
278
- integrationsInterModuleContract.methods.createCheckoutSpec,
279
- 'connection-inactive',
280
- {connectionId: project.sourceConnectionId},
281
- ),
282
- );
365
+ test.each([
366
+ 'pending',
367
+ 'succeeded',
368
+ 'failed',
369
+ 'cancelled',
370
+ 'skipped',
371
+ ] as const)('returns 409 when the checkout step is %s', async (status) => {
372
+ const {job, step} = await createRunningCheckoutStep({status});
373
+ const token = await mintActiveLeaseToken({jobId: job.id});
374
+
375
+ const res = await app.inject({
376
+ method: 'POST',
377
+ url: checkoutUrl(step.id, step.currentAttempt),
378
+ headers: {authorization: `Bearer ${token}`},
379
+ });
380
+
381
+ expect(res.statusCode).toBe(409);
382
+ expect(res.json().code).toBe('step-not-running');
383
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
384
+ });
385
+
386
+ test('returns 409 when the leased step is not a checkout step', async () => {
387
+ const {job, step} = await createRunningCheckoutStep({kind: 'run'});
283
388
  const token = await mintActiveLeaseToken({jobId: job.id});
284
389
 
285
390
  const res = await app.inject({
286
391
  method: 'POST',
287
- url: URL,
392
+ url: checkoutUrl(step.id, step.currentAttempt),
288
393
  headers: {authorization: `Bearer ${token}`},
289
394
  });
290
395
 
291
- expect(res.statusCode).toBe(422);
396
+ expect(res.statusCode).toBe(409);
397
+ expect(res.json().code).toBe('step-not-checkout');
398
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
292
399
  });
293
400
 
294
- test('maps an unsupported-checkout provider error to 422', async () => {
295
- const project = projectFactory.build();
401
+ test('refuses a target outside the workspace while the lease remains active', async () => {
402
+ const {project, job, step} = await createRunningCheckoutStep({
403
+ kind: 'checkout',
404
+ checkout: {project: crypto.randomUUID()},
405
+ });
296
406
  getProjectById.mockResolvedValue({project});
297
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
298
- createCheckoutSpec.mockRejectedValue(
407
+ resolveCheckoutTarget.mockRejectedValue(
299
408
  createInterModuleKnownError(
300
- integrationsInterModuleContract.methods.createCheckoutSpec,
301
- 'checkout-unsupported',
302
- {provider: 'github'},
409
+ projectsInterModuleContract.methods.resolveCheckoutTarget,
410
+ 'checkout-repository-not-authorized',
411
+ {},
303
412
  ),
304
413
  );
305
414
  const token = await mintActiveLeaseToken({jobId: job.id});
306
415
 
307
416
  const res = await app.inject({
308
417
  method: 'POST',
309
- url: URL,
418
+ url: checkoutUrl(step.id, step.currentAttempt),
310
419
  headers: {authorization: `Bearer ${token}`},
311
420
  });
312
421
 
313
- expect(res.statusCode).toBe(422);
314
- expect(res.json().code).toBe('integration-checkout-unsupported');
422
+ expect(res.statusCode).toBe(404);
423
+ expect(res.json().code).toBe('checkout-repository-not-authorized');
424
+ expect(createCheckoutSpec).not.toHaveBeenCalled();
315
425
  });
316
426
 
317
- test('surfaces an unexpected provider error as a 500 server-error', async () => {
318
- const project = projectFactory.build();
427
+ test('maps provider rate limiting to 429 without leaking credentials', async () => {
428
+ const {project, job, step} = await createRunningCheckoutStep();
319
429
  getProjectById.mockResolvedValue({project});
320
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
321
- createCheckoutSpec.mockRejectedValue(new Error('unexpected provider failure'));
430
+ resolveCheckoutTarget.mockResolvedValue({
431
+ projectId: project.id,
432
+ connectionId: project.sourceConnectionId,
433
+ externalRepositoryId: project.sourceExternalRepositoryId,
434
+ });
435
+ createCheckoutSpec.mockRejectedValue(
436
+ createInterModuleKnownError(
437
+ integrationsInterModuleContract.methods.createCheckoutSpec,
438
+ 'provider-failure',
439
+ {reason: 'rate-limited', retryAfterSeconds: 60},
440
+ ),
441
+ );
322
442
  const token = await mintActiveLeaseToken({jobId: job.id});
323
443
 
324
444
  const res = await app.inject({
325
445
  method: 'POST',
326
- url: URL,
446
+ url: checkoutUrl(step.id, step.currentAttempt),
327
447
  headers: {authorization: `Bearer ${token}`},
328
448
  });
329
449
 
330
- expect(res.statusCode).toBe(500);
331
- expect(res.json().code).toBe('server-error');
450
+ expect(res.statusCode).toBe(429);
451
+ expect(res.json().code).toBe('rate-limited');
452
+ expect(res.json().details.retry_after_seconds).toBe(60);
332
453
  });
333
454
 
334
455
  test('never writes the minted token to a log line', async () => {
335
- const project = projectFactory.build();
456
+ const {project, job, step} = await createRunningCheckoutStep();
336
457
  getProjectById.mockResolvedValue({project});
337
- const job = await jobFactory.create({}, {transient: {projectId: project.id}});
458
+ resolveCheckoutTarget.mockResolvedValue({
459
+ projectId: project.id,
460
+ connectionId: project.sourceConnectionId,
461
+ externalRepositoryId: project.sourceExternalRepositoryId,
462
+ });
338
463
  const secret = 'ghs-super-secret-token-value';
339
464
  createCheckoutSpec.mockResolvedValue(githubSpec(secret));
340
465
  const token = await mintActiveLeaseToken({jobId: job.id});
341
466
 
342
467
  const res = await app.inject({
343
468
  method: 'POST',
344
- url: URL,
469
+ url: checkoutUrl(step.id, step.currentAttempt),
345
470
  headers: {authorization: `Bearer ${token}`},
346
471
  });
347
472
 
@@ -349,4 +474,94 @@ describe('POST /runs/jobs/current/checkout-token', () => {
349
474
  expect(res.json().auth.token).toBe(secret);
350
475
  expect(logLines.join('\n')).not.toContain(secret);
351
476
  });
477
+
478
+ test('uses the active lease identity when a newer lease has replaced it', async () => {
479
+ const {run, project, job, step} = await createRunningCheckoutStep();
480
+ await insertRunningJobLease({
481
+ workspaceId: project.workspaceId,
482
+ workflowRunId: run.id,
483
+ workflowRunAttemptId: job.workflowRunAttemptId,
484
+ jobId: job.id,
485
+ jobExecutionId: step.jobExecutionId,
486
+ projectId: project.id,
487
+ runnerSessionId: crypto.randomUUID(),
488
+ });
489
+ const token = await mintLeaseToken({
490
+ jobId: job.id,
491
+ jobExecutionId: step.jobExecutionId,
492
+ runnerSessionId: crypto.randomUUID(),
493
+ });
494
+
495
+ const res = await app.inject({
496
+ method: 'POST',
497
+ url: checkoutUrl(step.id, step.currentAttempt),
498
+ headers: {authorization: `Bearer ${token}`},
499
+ });
500
+
501
+ expect(res.statusCode).toBe(404);
502
+ expect(res.json().code).toBe('lease-not-active');
503
+ });
352
504
  });
505
+
506
+ function checkoutUrl(stepId: string, attempt: number): string {
507
+ return `/runs/jobs/current/steps/${stepId}/checkout-token?attempt=${attempt}`;
508
+ }
509
+
510
+ type CheckoutConfig = {
511
+ project?: string;
512
+ connection?: string;
513
+ repository?: string;
514
+ ref?: string;
515
+ fetchDepth?: number;
516
+ permissions?: {contents: 'read' | 'write'};
517
+ persistCredentials?: boolean;
518
+ };
519
+
520
+ async function createRunningCheckoutStep(
521
+ options: {
522
+ kind?: 'setup' | 'checkout' | 'run';
523
+ checkout?: CheckoutConfig;
524
+ status?: StepStatus;
525
+ } = {},
526
+ ) {
527
+ const project = projectFactory.build();
528
+ const checkout = {
529
+ ...(options.checkout ?? {}),
530
+ fetchDepth: options.checkout?.fetchDepth ?? 1,
531
+ permissions: options.checkout?.permissions ?? {contents: 'read'},
532
+ persistCredentials: options.checkout?.persistCredentials ?? true,
533
+ } satisfies NonNullable<
534
+ Extract<WorkflowModel['jobs'][number]['steps'][number], {kind: 'checkout'}>['checkout']
535
+ >;
536
+ const steps = options.kind === 'checkout' ? [{checkout}] : [{run: 'echo hello'}];
537
+ const run = await createWorkflowRun({
538
+ workspaceId: project.workspaceId,
539
+ projectId: project.id,
540
+ definitionId: crypto.randomUUID(),
541
+ model: workflowModel({jobs: {build: {steps}}}),
542
+ triggerPayload: {
543
+ source: 'manual',
544
+ event: 'fire',
545
+ subscriptionId: crypto.randomUUID(),
546
+ userId: crypto.randomUUID(),
547
+ },
548
+ });
549
+ const [job] = await getJobsByWorkflowRunId(run.id);
550
+ if (!job) throw new Error('Expected workflow job');
551
+ await db().update(jobsTable).set({status: 'running'}).where(eq(jobsTable.id, job.id));
552
+
553
+ const stepRows = await getStepsByJobId(job.id);
554
+ const targetType =
555
+ options.kind === 'checkout' ? 'checkout' : options.kind === 'run' ? 'run' : 'setup';
556
+ const step = stepRows.find((candidate) => candidate.type === targetType);
557
+ if (!step) throw new Error(`Expected ${targetType} step`);
558
+ const status = options.status ?? 'running';
559
+ await db().update(stepsTable).set({status}).where(eq(stepsTable.id, step.id));
560
+
561
+ return {
562
+ run,
563
+ project,
564
+ job: {...job, status: 'running' as const},
565
+ step: {...step, status},
566
+ };
567
+ }