@storyshelf/core 0.3.1 → 0.5.3

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 (393) hide show
  1. package/README.md +38 -31
  2. package/dist/adapter/capture-queue.d.mts +29 -1
  3. package/dist/adapter/capture-runner.d.mts +18 -2
  4. package/dist/adapter/capture-runner.mjs +12 -1
  5. package/dist/adapter/capture-runner.mjs.map +1 -0
  6. package/dist/adapter/database.d.mts +67 -17
  7. package/dist/adapter/database.mjs +22 -55
  8. package/dist/adapter/database.mjs.map +1 -1
  9. package/dist/adapter/email-sender.d.mts +27 -0
  10. package/dist/adapter/email-sender.mjs +4 -0
  11. package/dist/adapter/metadata.d.mts +21 -15
  12. package/dist/adapter/notifier.d.mts +7 -0
  13. package/dist/adapter/notifier.mjs +6 -0
  14. package/dist/adapter/setup.d.mts +61 -0
  15. package/dist/adapter/setup.mjs +29 -0
  16. package/dist/adapter/setup.mjs.map +1 -0
  17. package/dist/adapter/webhook-events.d.mts +29 -0
  18. package/dist/adapter/webhook-events.mjs +83 -0
  19. package/dist/adapter/webhook-events.mjs.map +1 -0
  20. package/dist/adapters/notifier/events.d.mts +26 -0
  21. package/dist/adapters/notifier/events.mjs +66 -0
  22. package/dist/adapters/notifier/events.mjs.map +1 -0
  23. package/dist/adapters/notifier/format.d.mts +14 -0
  24. package/dist/adapters/notifier/format.mjs +96 -0
  25. package/dist/adapters/notifier/format.mjs.map +1 -0
  26. package/dist/adapters/notifier/provider.d.mts +32 -0
  27. package/dist/adapters/notifier/types.d.mts +56 -0
  28. package/dist/adapters/setup-runner.d.mts +23 -0
  29. package/dist/adapters/setup-runner.mjs +149 -0
  30. package/dist/adapters/setup-runner.mjs.map +1 -0
  31. package/dist/adapters/validate.d.mts +30 -0
  32. package/dist/adapters/validate.mjs +313 -0
  33. package/dist/adapters/validate.mjs.map +1 -0
  34. package/dist/auth.d.mts +94 -0
  35. package/dist/auth.mjs +4 -0
  36. package/dist/brand-packs.d.mts +16 -0
  37. package/dist/brand-packs.mjs +130 -0
  38. package/dist/brand-packs.mjs.map +1 -0
  39. package/dist/capture/adapter.d.mts +51 -1
  40. package/dist/capture/adapter.mjs.map +1 -1
  41. package/dist/capture/affected.d.mts +16 -0
  42. package/dist/capture/affected.mjs +64 -0
  43. package/dist/capture/affected.mjs.map +1 -0
  44. package/dist/capture/diff-record.mjs +30 -0
  45. package/dist/capture/diff-record.mjs.map +1 -0
  46. package/dist/capture/dispatch.d.mts +36 -0
  47. package/dist/capture/dispatch.mjs +88 -31
  48. package/dist/capture/dispatch.mjs.map +1 -1
  49. package/dist/capture/guards.mjs +31 -0
  50. package/dist/capture/guards.mjs.map +1 -0
  51. package/dist/capture/orchestrator.d.mts +41 -6
  52. package/dist/capture/orchestrator.mjs +151 -28
  53. package/dist/capture/orchestrator.mjs.map +1 -1
  54. package/dist/capture/pipeline.d.mts +30 -16
  55. package/dist/capture/pipeline.mjs +143 -39
  56. package/dist/capture/pipeline.mjs.map +1 -1
  57. package/dist/capture/recovery.d.mts +21 -0
  58. package/dist/capture/recovery.mjs +83 -0
  59. package/dist/capture/recovery.mjs.map +1 -0
  60. package/dist/capture/rediff.d.mts +35 -0
  61. package/dist/capture/rediff.mjs +77 -0
  62. package/dist/capture/rediff.mjs.map +1 -0
  63. package/dist/capture/sizing.d.mts +58 -0
  64. package/dist/capture/sizing.mjs +119 -0
  65. package/dist/capture/sizing.mjs.map +1 -0
  66. package/dist/capture/skip-checks.mjs +3 -3
  67. package/dist/capture/skip-checks.mjs.map +1 -1
  68. package/dist/capture/statics.d.mts +38 -0
  69. package/dist/capture/statics.mjs +38 -4
  70. package/dist/capture/statics.mjs.map +1 -1
  71. package/dist/capture/status-fanout.mjs +7 -2
  72. package/dist/capture/status-fanout.mjs.map +1 -1
  73. package/dist/capture/storybook.d.mts +31 -2
  74. package/dist/capture/storybook.mjs +77 -2
  75. package/dist/capture/storybook.mjs.map +1 -1
  76. package/dist/capture.d.mts +10 -4
  77. package/dist/capture.mjs +7 -2
  78. package/dist/config.d.mts +121 -4
  79. package/dist/config.mjs +55 -8
  80. package/dist/config.mjs.map +1 -1
  81. package/dist/index.d.mts +5 -56
  82. package/dist/index.mjs +4 -284
  83. package/dist/logger.d.mts +26 -17
  84. package/dist/logger.mjs +10 -5
  85. package/dist/logger.mjs.map +1 -1
  86. package/dist/models/baseline-status.d.mts +29 -0
  87. package/dist/models/baseline-status.mjs +26 -0
  88. package/dist/models/baseline-status.mjs.map +1 -0
  89. package/dist/models/baseline.d.mts +33 -0
  90. package/dist/models/baseline.mjs +35 -13
  91. package/dist/models/baseline.mjs.map +1 -1
  92. package/dist/models/build.d.mts +87 -0
  93. package/dist/models/build.mjs +83 -35
  94. package/dist/models/build.mjs.map +1 -1
  95. package/dist/models/capture-attempt.d.mts +40 -0
  96. package/dist/models/capture-attempt.mjs +85 -0
  97. package/dist/models/capture-attempt.mjs.map +1 -0
  98. package/dist/models/capture-log.d.mts +21 -0
  99. package/dist/models/capture-log.mjs +44 -0
  100. package/dist/models/capture-log.mjs.map +1 -0
  101. package/dist/models/comment.d.mts +29 -0
  102. package/dist/models/comment.mjs +11 -8
  103. package/dist/models/comment.mjs.map +1 -1
  104. package/dist/models/content-ref.d.mts +21 -0
  105. package/dist/models/content-ref.mjs +49 -0
  106. package/dist/models/content-ref.mjs.map +1 -0
  107. package/dist/models/credentials.d.mts +36 -0
  108. package/dist/models/credentials.mjs +103 -0
  109. package/dist/models/credentials.mjs.map +1 -0
  110. package/dist/models/label.d.mts +58 -0
  111. package/dist/models/label.mjs +32 -27
  112. package/dist/models/label.mjs.map +1 -1
  113. package/dist/models/member.d.mts +34 -0
  114. package/dist/models/member.mjs +20 -11
  115. package/dist/models/member.mjs.map +1 -1
  116. package/dist/models/notification-channel.d.mts +52 -0
  117. package/dist/models/notification-channel.mjs +93 -0
  118. package/dist/models/notification-channel.mjs.map +1 -0
  119. package/dist/models/notification-subscription.d.mts +44 -0
  120. package/dist/models/notification-subscription.mjs +89 -0
  121. package/dist/models/notification-subscription.mjs.map +1 -0
  122. package/dist/models/project-group-mapping.d.mts +30 -0
  123. package/dist/models/project-group-mapping.mjs +46 -0
  124. package/dist/models/project-group-mapping.mjs.map +1 -0
  125. package/dist/models/project.d.mts +54 -0
  126. package/dist/models/project.mjs +15 -11
  127. package/dist/models/project.mjs.map +1 -1
  128. package/dist/models/snapshot.d.mts +58 -0
  129. package/dist/models/snapshot.mjs +13 -7
  130. package/dist/models/snapshot.mjs.map +1 -1
  131. package/dist/models/status-config.d.mts +38 -0
  132. package/dist/models/status-config.mjs +12 -11
  133. package/dist/models/status-config.mjs.map +1 -1
  134. package/dist/models/token.d.mts +44 -0
  135. package/dist/models/token.mjs +18 -15
  136. package/dist/models/token.mjs.map +1 -1
  137. package/dist/models/user.d.mts +42 -0
  138. package/dist/models/user.mjs +65 -0
  139. package/dist/models/user.mjs.map +1 -0
  140. package/dist/models/webhook.d.mts +50 -0
  141. package/dist/models/webhook.mjs +12 -10
  142. package/dist/models/webhook.mjs.map +1 -1
  143. package/dist/models.d.mts +22 -0
  144. package/dist/models.mjs +23 -0
  145. package/dist/paths.mjs +29 -0
  146. package/dist/paths.mjs.map +1 -1
  147. package/dist/retention.d.mts +45 -0
  148. package/dist/retention.mjs +145 -0
  149. package/dist/retention.mjs.map +1 -0
  150. package/dist/schema/baseline.d.mts +1 -0
  151. package/dist/schema/build.d.mts +8 -0
  152. package/dist/schema/capture-attempt.d.mts +38 -0
  153. package/dist/schema/comment.d.mts +1 -1
  154. package/dist/schema/content-ref.d.mts +13 -0
  155. package/dist/schema/member.d.mts +2 -0
  156. package/dist/schema/notification.d.mts +35 -0
  157. package/dist/schema/project-group-mapping.d.mts +15 -0
  158. package/dist/schema/project.d.mts +4 -0
  159. package/dist/schema/snapshot.d.mts +7 -0
  160. package/dist/schema/token.d.mts +2 -0
  161. package/dist/schema/user.d.mts +11 -1
  162. package/dist/schema.d.mts +9 -46
  163. package/dist/schema.mjs +1 -55
  164. package/dist/test-helpers/create-project.d.mts +15 -0
  165. package/dist/test-helpers/create-project.mjs +28 -0
  166. package/dist/test-helpers/create-project.mjs.map +1 -0
  167. package/dist/test-helpers/database-contract.d.mts +13 -0
  168. package/dist/test-helpers/database-contract.mjs +58 -0
  169. package/dist/test-helpers/database-contract.mjs.map +1 -0
  170. package/dist/test-helpers/fake-adapters.d.mts +14 -0
  171. package/dist/test-helpers/fake-adapters.mjs +20 -0
  172. package/dist/test-helpers/fake-adapters.mjs.map +1 -0
  173. package/dist/test-helpers/fake-database.d.mts +11 -0
  174. package/dist/test-helpers/fake-database.mjs +104 -0
  175. package/dist/test-helpers/fake-database.mjs.map +1 -0
  176. package/dist/test-helpers/fake-storage.d.mts +14 -0
  177. package/dist/test-helpers/fake-storage.mjs +49 -0
  178. package/dist/test-helpers/fake-storage.mjs.map +1 -0
  179. package/dist/test-helpers/fake-tables.mjs +276 -0
  180. package/dist/test-helpers/fake-tables.mjs.map +1 -0
  181. package/dist/test-helpers/git-contract.d.mts +11 -0
  182. package/dist/test-helpers/git-contract.mjs +34 -0
  183. package/dist/test-helpers/git-contract.mjs.map +1 -0
  184. package/dist/test-helpers/notifier-contract.d.mts +11 -0
  185. package/dist/test-helpers/notifier-contract.mjs +34 -0
  186. package/dist/test-helpers/notifier-contract.mjs.map +1 -0
  187. package/dist/test-helpers/queue-contract.d.mts +11 -0
  188. package/dist/test-helpers/queue-contract.mjs +64 -0
  189. package/dist/test-helpers/queue-contract.mjs.map +1 -0
  190. package/dist/test-helpers/runner-contract.d.mts +11 -0
  191. package/dist/test-helpers/runner-contract.mjs +32 -0
  192. package/dist/test-helpers/runner-contract.mjs.map +1 -0
  193. package/dist/test-helpers/sql-chunks.mjs +138 -0
  194. package/dist/test-helpers/sql-chunks.mjs.map +1 -0
  195. package/dist/test-helpers/storage-contract.d.mts +11 -0
  196. package/dist/test-helpers/storage-contract.mjs +87 -0
  197. package/dist/test-helpers/storage-contract.mjs.map +1 -0
  198. package/dist/test-helpers.d.mts +13 -0
  199. package/dist/test-helpers.mjs +14 -0
  200. package/dist/tracing.mjs +63 -0
  201. package/dist/tracing.mjs.map +1 -0
  202. package/dist/types.d.mts +38 -5
  203. package/dist/types.mjs +18 -5
  204. package/dist/types.mjs.map +1 -1
  205. package/dist/urls.d.mts +12 -1
  206. package/dist/urls.mjs +22 -3
  207. package/dist/urls.mjs.map +1 -1
  208. package/dist/utils/encrypt.d.mts +48 -0
  209. package/dist/utils/encrypt.mjs +67 -13
  210. package/dist/utils/encrypt.mjs.map +1 -1
  211. package/dist/utils/hash.d.mts +17 -0
  212. package/dist/utils/hash.mjs +8 -2
  213. package/dist/utils/hash.mjs.map +1 -1
  214. package/dist/utils/http.d.mts +58 -0
  215. package/dist/utils/http.mjs +147 -0
  216. package/dist/utils/http.mjs.map +1 -0
  217. package/dist/utils/mime.d.mts +9 -0
  218. package/dist/utils/mime.mjs +33 -0
  219. package/dist/utils/mime.mjs.map +1 -0
  220. package/dist/utils/path-security.d.mts +12 -0
  221. package/dist/utils/path-security.mjs +24 -0
  222. package/dist/utils/path-security.mjs.map +1 -0
  223. package/dist/utils/redact.d.mts +17 -0
  224. package/dist/utils/redact.mjs +42 -0
  225. package/dist/utils/redact.mjs.map +1 -0
  226. package/dist/utils/timing.d.mts +19 -0
  227. package/dist/utils/timing.mjs +46 -0
  228. package/dist/utils/timing.mjs.map +1 -0
  229. package/dist/utils/ulid.d.mts +10 -0
  230. package/dist/utils/ulid.mjs +8 -8
  231. package/dist/utils/ulid.mjs.map +1 -1
  232. package/dist/utils.d.mts +12 -0
  233. package/dist/utils.mjs +13 -0
  234. package/package.json +69 -28
  235. package/dist/adapter/auth.d.mts +0 -50
  236. package/dist/adapter/auth.mjs +0 -10
  237. package/dist/adapter/auth.mjs.map +0 -1
  238. package/dist/adapter/init.d.mts +0 -58
  239. package/dist/adapter/init.mjs +0 -93
  240. package/dist/adapter/init.mjs.map +0 -1
  241. package/dist/adapters/webhook-events.mjs +0 -55
  242. package/dist/adapters/webhook-events.mjs.map +0 -1
  243. package/dist/ddl.d.mts +0 -8
  244. package/dist/ddl.mjs +0 -158
  245. package/dist/ddl.mjs.map +0 -1
  246. package/dist/index.mjs.map +0 -1
  247. package/dist/middleware/auth-gate.mjs +0 -36
  248. package/dist/middleware/auth-gate.mjs.map +0 -1
  249. package/dist/middleware/csrf.mjs +0 -40
  250. package/dist/middleware/csrf.mjs.map +0 -1
  251. package/dist/middleware/rate-limit.mjs +0 -39
  252. package/dist/middleware/rate-limit.mjs.map +0 -1
  253. package/dist/middleware/request-log.mjs +0 -28
  254. package/dist/middleware/request-log.mjs.map +0 -1
  255. package/dist/middleware/store-scope.mjs +0 -29
  256. package/dist/middleware/store-scope.mjs.map +0 -1
  257. package/dist/pages/build-detail.mjs +0 -339
  258. package/dist/pages/build-detail.mjs.map +0 -1
  259. package/dist/pages/build-diff-comments.mjs +0 -113
  260. package/dist/pages/build-diff-comments.mjs.map +0 -1
  261. package/dist/pages/build-diff-header.mjs +0 -163
  262. package/dist/pages/build-diff-header.mjs.map +0 -1
  263. package/dist/pages/build-diff-nav.mjs +0 -66
  264. package/dist/pages/build-diff-nav.mjs.map +0 -1
  265. package/dist/pages/build-diff-viewer.mjs +0 -197
  266. package/dist/pages/build-diff-viewer.mjs.map +0 -1
  267. package/dist/pages/build-diff.mjs +0 -90
  268. package/dist/pages/build-diff.mjs.map +0 -1
  269. package/dist/pages/compute-jobs.mjs +0 -198
  270. package/dist/pages/compute-jobs.mjs.map +0 -1
  271. package/dist/pages/label-detail.mjs +0 -228
  272. package/dist/pages/label-detail.mjs.map +0 -1
  273. package/dist/pages/login.mjs +0 -78
  274. package/dist/pages/login.mjs.map +0 -1
  275. package/dist/pages/project-builds.mjs +0 -231
  276. package/dist/pages/project-builds.mjs.map +0 -1
  277. package/dist/pages/project-create.mjs +0 -169
  278. package/dist/pages/project-create.mjs.map +0 -1
  279. package/dist/pages/project-settings.mjs +0 -102
  280. package/dist/pages/project-settings.mjs.map +0 -1
  281. package/dist/pages/projects.mjs +0 -184
  282. package/dist/pages/projects.mjs.map +0 -1
  283. package/dist/pages/root.mjs +0 -94
  284. package/dist/pages/root.mjs.map +0 -1
  285. package/dist/pages/settings-general.mjs +0 -192
  286. package/dist/pages/settings-general.mjs.map +0 -1
  287. package/dist/pages/settings-labels.mjs +0 -150
  288. package/dist/pages/settings-labels.mjs.map +0 -1
  289. package/dist/pages/settings-members.mjs +0 -125
  290. package/dist/pages/settings-members.mjs.map +0 -1
  291. package/dist/pages/settings-status.mjs +0 -152
  292. package/dist/pages/settings-status.mjs.map +0 -1
  293. package/dist/pages/settings-tests.mjs +0 -149
  294. package/dist/pages/settings-tests.mjs.map +0 -1
  295. package/dist/pages/settings-tokens.mjs +0 -96
  296. package/dist/pages/settings-tokens.mjs.map +0 -1
  297. package/dist/pages/settings-webhooks.mjs +0 -150
  298. package/dist/pages/settings-webhooks.mjs.map +0 -1
  299. package/dist/pages/storybook.mjs +0 -96
  300. package/dist/pages/storybook.mjs.map +0 -1
  301. package/dist/retention/purge.mjs +0 -67
  302. package/dist/retention/purge.mjs.map +0 -1
  303. package/dist/routers/admin.mjs +0 -42
  304. package/dist/routers/admin.mjs.map +0 -1
  305. package/dist/routers/assets.mjs +0 -25
  306. package/dist/routers/assets.mjs.map +0 -1
  307. package/dist/routers/auth.mjs +0 -78
  308. package/dist/routers/auth.mjs.map +0 -1
  309. package/dist/routers/builds.handlers.mjs +0 -184
  310. package/dist/routers/builds.handlers.mjs.map +0 -1
  311. package/dist/routers/builds.mjs +0 -185
  312. package/dist/routers/builds.mjs.map +0 -1
  313. package/dist/routers/comments.mjs +0 -90
  314. package/dist/routers/comments.mjs.map +0 -1
  315. package/dist/routers/health.mjs +0 -137
  316. package/dist/routers/health.mjs.map +0 -1
  317. package/dist/routers/helpers.mjs +0 -82
  318. package/dist/routers/helpers.mjs.map +0 -1
  319. package/dist/routers/htmx.mjs +0 -20
  320. package/dist/routers/htmx.mjs.map +0 -1
  321. package/dist/routers/labels.mjs +0 -110
  322. package/dist/routers/labels.mjs.map +0 -1
  323. package/dist/routers/media.mjs +0 -55
  324. package/dist/routers/media.mjs.map +0 -1
  325. package/dist/routers/members.mjs +0 -102
  326. package/dist/routers/members.mjs.map +0 -1
  327. package/dist/routers/projects.mjs +0 -122
  328. package/dist/routers/projects.mjs.map +0 -1
  329. package/dist/routers/schemas.mjs +0 -224
  330. package/dist/routers/schemas.mjs.map +0 -1
  331. package/dist/routers/settings-general.mjs +0 -95
  332. package/dist/routers/settings-general.mjs.map +0 -1
  333. package/dist/routers/settings-labels.mjs +0 -49
  334. package/dist/routers/settings-labels.mjs.map +0 -1
  335. package/dist/routers/settings-members.mjs +0 -30
  336. package/dist/routers/settings-members.mjs.map +0 -1
  337. package/dist/routers/settings-status.mjs +0 -73
  338. package/dist/routers/settings-status.mjs.map +0 -1
  339. package/dist/routers/settings-tokens.mjs +0 -40
  340. package/dist/routers/settings-tokens.mjs.map +0 -1
  341. package/dist/routers/settings-webhooks.mjs +0 -63
  342. package/dist/routers/settings-webhooks.mjs.map +0 -1
  343. package/dist/routers/settings.handlers.mjs +0 -93
  344. package/dist/routers/settings.handlers.mjs.map +0 -1
  345. package/dist/routers/settings.mjs +0 -23
  346. package/dist/routers/settings.mjs.map +0 -1
  347. package/dist/routers/snapshots.mjs +0 -144
  348. package/dist/routers/snapshots.mjs.map +0 -1
  349. package/dist/routers/status-configs.mjs +0 -101
  350. package/dist/routers/status-configs.mjs.map +0 -1
  351. package/dist/routers/storybook.mjs +0 -120
  352. package/dist/routers/storybook.mjs.map +0 -1
  353. package/dist/routers/tokens.mjs +0 -81
  354. package/dist/routers/tokens.mjs.map +0 -1
  355. package/dist/routers/ui.mjs +0 -165
  356. package/dist/routers/ui.mjs.map +0 -1
  357. package/dist/routers/webhooks.mjs +0 -90
  358. package/dist/routers/webhooks.mjs.map +0 -1
  359. package/dist/schema/baseline.mjs +0 -22
  360. package/dist/schema/baseline.mjs.map +0 -1
  361. package/dist/schema/build.mjs +0 -29
  362. package/dist/schema/build.mjs.map +0 -1
  363. package/dist/schema/comment.mjs +0 -26
  364. package/dist/schema/comment.mjs.map +0 -1
  365. package/dist/schema/label.mjs +0 -30
  366. package/dist/schema/label.mjs.map +0 -1
  367. package/dist/schema/member.mjs +0 -19
  368. package/dist/schema/member.mjs.map +0 -1
  369. package/dist/schema/project.mjs +0 -25
  370. package/dist/schema/project.mjs.map +0 -1
  371. package/dist/schema/snapshot.mjs +0 -34
  372. package/dist/schema/snapshot.mjs.map +0 -1
  373. package/dist/schema/status-config.mjs +0 -20
  374. package/dist/schema/status-config.mjs.map +0 -1
  375. package/dist/schema/token.mjs +0 -19
  376. package/dist/schema/token.mjs.map +0 -1
  377. package/dist/schema/user.mjs +0 -19
  378. package/dist/schema/user.mjs.map +0 -1
  379. package/dist/schema/webhook.mjs +0 -20
  380. package/dist/schema/webhook.mjs.map +0 -1
  381. package/dist/schema.mjs.map +0 -1
  382. package/dist/store.mjs +0 -20
  383. package/dist/store.mjs.map +0 -1
  384. package/dist/ui/document.mjs +0 -348
  385. package/dist/ui/document.mjs.map +0 -1
  386. package/dist/ui/feedback.mjs +0 -24
  387. package/dist/ui/feedback.mjs.map +0 -1
  388. package/dist/ui/forms.mjs +0 -117
  389. package/dist/ui/forms.mjs.map +0 -1
  390. package/dist/ui/styles.mjs +0 -174
  391. package/dist/ui/styles.mjs.map +0 -1
  392. package/dist/ui/theme.mjs +0 -44
  393. package/dist/ui/theme.mjs.map +0 -1
@@ -2,14 +2,65 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
+ import { CaptureAttemptModel, emitAttemptLog } from "../models/capture-attempt.mjs";
5
6
  import { ProjectModel } from "../models/project.mjs";
6
7
  import { executeCaptureJob } from "./orchestrator.mjs";
7
- import { hasApprovedBuildForSha, isAlreadyMerged } from "./skip-checks.mjs";
8
8
  import { postStatusesForBuild } from "./status-fanout.mjs";
9
+ import { CaptureLogModel } from "../models/capture-log.mjs";
10
+ import { hasApprovedBuildForSha, isAlreadyMerged } from "./skip-checks.mjs";
9
11
  //#region src/capture/dispatch.ts
12
+ /** Build the queue's job runner: post pending, apply skip rules, capture, report. */
13
+ function createDispatchJob(deps) {
14
+ return async (job) => {
15
+ const target = await loadJobTarget(deps, job);
16
+ if (!target) return;
17
+ const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;
18
+ await postPending(deps, target.project, pendingUrl, target.build.gitSha);
19
+ const { attempt, recordLog } = await beginAttempt(deps, target.project, target.build, job.reqId);
20
+ const ctx = {
21
+ deps,
22
+ build: target.build,
23
+ project: target.project,
24
+ pendingUrl,
25
+ attempt,
26
+ recordLog
27
+ };
28
+ if (await maybeSkipMerged(ctx) || await maybeSkipDuplicate(ctx)) return;
29
+ await runAndReport(ctx, job);
30
+ };
31
+ }
32
+ /** Open a new attempt row for this run and return its log recorder. */
33
+ async function beginAttempt(deps, project, build, reqId) {
34
+ const attempts = new CaptureAttemptModel(deps.db, deps.tables);
35
+ const logs = new CaptureLogModel(deps.db, deps.tables);
36
+ const attempt = await attempts.startAttempt(project.id, build.id, reqId);
37
+ const recordLog = async (level, message, fields) => {
38
+ await logs.append(project.id, build.id, attempt.id, level, message, fields);
39
+ };
40
+ await emitAttemptLog(recordLog, deps.logger, "info", "capture queued", {
41
+ buildId: build.id,
42
+ attemptNo: attempt.attemptNo
43
+ });
44
+ return {
45
+ attempt,
46
+ recordLog
47
+ };
48
+ }
49
+ /** Close the attempt row with its terminal status, counts, and error. */
50
+ async function finishAttempt(ctx, status, error, counts) {
51
+ const { deps, attempt } = ctx;
52
+ await new CaptureAttemptModel(deps.db, deps.tables).markFinished(attempt.id, status, error, counts);
53
+ await emitAttemptLog(ctx.recordLog, deps.logger, status === "failed" ? "error" : "info", `capture ${status}`, { attemptNo: attempt.attemptNo });
54
+ }
55
+ /** Full error text for an attempt row (message plus stack when available). */
56
+ function errorText(reason) {
57
+ if (reason instanceof Error) return reason.stack ?? reason.message;
58
+ return String(reason);
59
+ }
10
60
  async function postPending(deps, project, url, sha) {
11
61
  await postStatusesForBuild({
12
62
  db: deps.db,
63
+ tables: deps.tables,
13
64
  project,
14
65
  sha,
15
66
  status: "pending",
@@ -24,6 +75,7 @@ async function postPending(deps, project, url, sha) {
24
75
  async function postTerminal(deps, project, url, sha, status) {
25
76
  await postStatusesForBuild({
26
77
  db: deps.db,
78
+ tables: deps.tables,
27
79
  project,
28
80
  sha,
29
81
  status,
@@ -42,7 +94,12 @@ async function approveWithoutCapture(ctx, reason) {
42
94
  sha: build.gitSha,
43
95
  branch: build.gitBranch
44
96
  }, reason);
45
- await new BuildModel(deps.db).setStatus(build.id, "approved").catch(() => {});
97
+ await emitAttemptLog(ctx.recordLog, deps.logger, "info", reason, {
98
+ sha: build.gitSha,
99
+ branch: build.gitBranch
100
+ });
101
+ await finishAttempt(ctx, "completed");
102
+ await new BuildModel(deps.db, deps.tables).setStatus(build.id, "approved").catch(() => {});
46
103
  await postTerminal(deps, project, pendingUrl, build.gitSha, "success").catch(() => {});
47
104
  }
48
105
  async function maybeSkipMerged(ctx) {
@@ -54,6 +111,7 @@ async function maybeSkipMerged(ctx) {
54
111
  branch: build.gitBranch,
55
112
  secret: deps.secret,
56
113
  db: deps.db,
114
+ tables: deps.tables,
57
115
  projectId: project.id,
58
116
  logger: deps.logger
59
117
  }).catch(() => false)) {
@@ -64,7 +122,7 @@ async function maybeSkipMerged(ctx) {
64
122
  }
65
123
  async function maybeSkipDuplicate(ctx) {
66
124
  const { deps, build, project } = ctx;
67
- if (await hasApprovedBuildForSha(deps.db, project.id, build.gitSha, build.id)) {
125
+ if (await hasApprovedBuildForSha(deps.db, deps.tables, project.id, build.gitSha, build.id)) {
68
126
  await approveWithoutCapture(ctx, "skipping capture — duplicate sha already approved");
69
127
  return true;
70
128
  }
@@ -79,24 +137,38 @@ function terminalState(status) {
79
137
  /** Post the terminal status for a finished capture. */
80
138
  async function reportTerminalStatus(ctx, buildId) {
81
139
  const { deps, build, project, pendingUrl } = ctx;
82
- const terminal = terminalState((await new BuildModel(deps.db).get(buildId))?.status);
140
+ const terminal = terminalState((await new BuildModel(deps.db, deps.tables).get(buildId))?.status);
83
141
  if (terminal) await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);
84
142
  }
85
143
  async function runAndReport(ctx, job) {
86
- const { deps, build, project, pendingUrl } = ctx;
144
+ const { deps, build } = ctx;
145
+ await new CaptureAttemptModel(deps.db, deps.tables).markRunning(ctx.attempt.id);
146
+ await emitAttemptLog(ctx.recordLog, deps.logger, "info", "capture started", {
147
+ buildId: build.id,
148
+ attemptNo: ctx.attempt.attemptNo
149
+ });
87
150
  try {
88
- await executeCaptureJob({
151
+ await finishAttempt(ctx, "completed", void 0, await executeCaptureJob({
89
152
  buildId: job.buildId,
90
- reqId: job.reqId
91
- }, deps.jobOptions);
153
+ reqId: job.reqId,
154
+ traceparent: job.traceparent,
155
+ attempt: {
156
+ id: ctx.attempt.id,
157
+ attemptNo: ctx.attempt.attemptNo,
158
+ projectId: ctx.project.id
159
+ },
160
+ recordLog: ctx.recordLog
161
+ }, deps.jobOptions));
92
162
  await reportTerminalStatus(ctx, job.buildId);
93
163
  } catch (error) {
164
+ await finishAttempt(ctx, "failed", errorText(error));
94
165
  await postStatusesForBuild({
95
166
  db: deps.db,
96
- project,
167
+ tables: deps.tables,
168
+ project: ctx.project,
97
169
  sha: build.gitSha,
98
170
  status: "failure",
99
- url: pendingUrl,
171
+ url: ctx.pendingUrl,
100
172
  providers: deps.gitHosts,
101
173
  secret: deps.secret,
102
174
  logger: deps.logger
@@ -106,19 +178,21 @@ async function runAndReport(ctx, job) {
106
178
  }
107
179
  /** Load the build + project for a job, running orphan jobs directly. */
108
180
  async function loadJobTarget(deps, job) {
109
- const build = await new BuildModel(deps.db).get(job.buildId);
181
+ const build = await new BuildModel(deps.db, deps.tables).get(job.buildId);
110
182
  if (!build) {
111
183
  await executeCaptureJob({
112
184
  buildId: job.buildId,
113
- reqId: job.reqId
185
+ reqId: job.reqId,
186
+ traceparent: job.traceparent
114
187
  }, deps.jobOptions);
115
188
  return null;
116
189
  }
117
- const project = await new ProjectModel(deps.db).get(build.projectId);
190
+ const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);
118
191
  if (!project) {
119
192
  await executeCaptureJob({
120
193
  buildId: job.buildId,
121
- reqId: job.reqId
194
+ reqId: job.reqId,
195
+ traceparent: job.traceparent
122
196
  }, deps.jobOptions);
123
197
  return null;
124
198
  }
@@ -127,23 +201,6 @@ async function loadJobTarget(deps, job) {
127
201
  project
128
202
  };
129
203
  }
130
- /** Build the queue's job runner: post pending, apply skip rules, capture, report. */
131
- function createDispatchJob(deps) {
132
- return async (job) => {
133
- const target = await loadJobTarget(deps, job);
134
- if (!target) return;
135
- const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;
136
- await postPending(deps, target.project, pendingUrl, target.build.gitSha);
137
- const ctx = {
138
- deps,
139
- build: target.build,
140
- project: target.project,
141
- pendingUrl
142
- };
143
- if (await maybeSkipMerged(ctx) || await maybeSkipDuplicate(ctx)) return;
144
- await runAndReport(ctx, job);
145
- };
146
- }
147
204
  //#endregion
148
205
  export { createDispatchJob };
149
206
 
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.mjs","names":[],"sources":["../../src/capture/dispatch.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./orchestrator.ts\";\nimport { hasApprovedBuildForSha, isAlreadyMerged } from \"./skip-checks.ts\";\nimport { postStatusesForBuild } from \"./status-fanout.ts\";\n\n/** Dependencies for building the capture queue's job runner. */\nexport interface DispatchDeps {\n db: DatabaseAdapter;\n jobOptions: CaptureJobOptions;\n gitHosts: GitHostProvider[];\n secret: string | undefined;\n logger: Logger;\n}\n\n/** A queued capture job. */\nexport interface CaptureDispatchJob {\n buildId: string;\n reqId?: string;\n}\n\ninterface JobContext {\n deps: DispatchDeps;\n build: Build;\n project: Project;\n pendingUrl: string;\n}\n\nasync function postPending(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha,\n status: \"pending\",\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post pending status\");\n });\n}\n\nasync function postTerminal(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n status: \"success\" | \"failure\",\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha,\n status,\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post terminal status\");\n });\n}\n\nasync function approveWithoutCapture(ctx: JobContext, reason: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n deps.logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, reason);\n await new BuildModel(deps.db).setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postTerminal(deps, project, pendingUrl, build.gitSha, \"success\").catch(() => {}); // Intentionally empty — fire-and-forget\n}\n\nasync function maybeSkipMerged(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n if (build.isDefault) {\n return false;\n }\n const merged = await isAlreadyMerged({\n providers: deps.gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: deps.secret,\n db: deps.db,\n projectId: project.id,\n logger: deps.logger,\n }).catch(() => false);\n if (merged) {\n await approveWithoutCapture(ctx, \"skipping capture — already merged\");\n return true;\n }\n return false;\n}\n\nasync function maybeSkipDuplicate(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n const dup = await hasApprovedBuildForSha(deps.db, project.id, build.gitSha, build.id);\n if (dup) {\n await approveWithoutCapture(ctx, \"skipping capture — duplicate sha already approved\");\n return true;\n }\n return false;\n}\n\n/** Map a terminal build status to its check-run state. */\nfunction terminalState(status: string | undefined): \"success\" | \"failure\" | null {\n if (status === \"approved\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"failure\";\n }\n return null;\n}\n\n/** Post the terminal status for a finished capture. */\nasync function reportTerminalStatus(ctx: JobContext, buildId: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n const updated = await new BuildModel(deps.db).get(buildId);\n const terminal = terminalState(updated?.status);\n if (terminal) {\n await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);\n }\n}\n\nasync function runAndReport(ctx: JobContext, job: CaptureDispatchJob): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n try {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n await reportTerminalStatus(ctx, job.buildId);\n } catch (error: unknown) {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: pendingUrl,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n}\n\n/** Load the build + project for a job, running orphan jobs directly. */\nasync function loadJobTarget(\n deps: DispatchDeps,\n job: CaptureDispatchJob,\n): Promise<{ build: Build; project: Project } | null> {\n const builds = new BuildModel(deps.db);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n return null;\n }\n const project = await new ProjectModel(deps.db).get(build.projectId);\n if (!project) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n return null;\n }\n return { build, project };\n}\n\n/** Build the queue's job runner: post pending, apply skip rules, capture, report. */\nexport function createDispatchJob(deps: DispatchDeps): (job: CaptureDispatchJob) => Promise<void> {\n return async (job: CaptureDispatchJob): Promise<void> => {\n const target = await loadJobTarget(deps, job);\n if (!target) {\n return;\n }\n const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;\n await postPending(deps, target.project, pendingUrl, target.build.gitSha);\n const ctx: JobContext = { deps, build: target.build, project: target.project, pendingUrl };\n if ((await maybeSkipMerged(ctx)) || (await maybeSkipDuplicate(ctx))) {\n return;\n }\n await runAndReport(ctx, job);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.mjs","names":[],"sources":["../../src/capture/dispatch.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport {\n CaptureAttemptModel,\n emitAttemptLog,\n type AttemptLogRecorder,\n type CaptureAttemptTables,\n} from \"../models/capture-attempt.ts\";\nimport { CaptureLogModel, type CaptureLogTables } from \"../models/capture-log.ts\";\nimport { ProjectModel, type ProjectTables } from \"../models/project.ts\";\nimport type { StatusConfigTables } from \"../models/status-config.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { CaptureAttempt } from \"../schema/capture-attempt.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./orchestrator.ts\";\nimport { hasApprovedBuildForSha, isAlreadyMerged } from \"./skip-checks.ts\";\nimport { postStatusesForBuild } from \"./status-fanout.ts\";\n\n/** Build the queue's job runner: post pending, apply skip rules, capture, report. */\nexport function createDispatchJob(deps: DispatchDeps): (job: CaptureDispatchJob) => Promise<void> {\n return async (job: CaptureDispatchJob): Promise<void> => {\n const target = await loadJobTarget(deps, job);\n if (!target) {\n return;\n }\n const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;\n await postPending(deps, target.project, pendingUrl, target.build.gitSha);\n const { attempt, recordLog } = await beginAttempt(\n deps,\n target.project,\n target.build,\n job.reqId,\n );\n const ctx: JobContext = {\n deps,\n build: target.build,\n project: target.project,\n pendingUrl,\n attempt,\n recordLog,\n };\n if ((await maybeSkipMerged(ctx)) || (await maybeSkipDuplicate(ctx))) {\n return;\n }\n await runAndReport(ctx, job);\n };\n}\n\n/** Table handles required by dispatch. */\nexport type DispatchTables = BuildTables &\n ProjectTables &\n StatusConfigTables &\n CaptureAttemptTables &\n CaptureLogTables;\n\n/** Dependencies for building the capture queue's job runner. */\nexport interface DispatchDeps {\n db: DatabaseAdapter;\n tables: DispatchTables;\n jobOptions: CaptureJobOptions;\n gitHosts: GitHostProvider[];\n secret: SecretInput;\n logger: Logger;\n}\n\n/** A queued capture job. */\nexport interface CaptureDispatchJob {\n buildId: string;\n reqId?: string;\n /** W3C `traceparent` continuing the enqueueing request's trace. */\n traceparent?: string;\n}\n\ninterface JobContext {\n deps: DispatchDeps;\n build: Build;\n project: Project;\n pendingUrl: string;\n attempt: CaptureAttempt;\n recordLog: AttemptLogRecorder;\n}\n\n/** Open a new attempt row for this run and return its log recorder. */\nasync function beginAttempt(\n deps: DispatchDeps,\n project: Project,\n build: Build,\n reqId: string | undefined,\n): Promise<{ attempt: CaptureAttempt; recordLog: AttemptLogRecorder }> {\n const attempts = new CaptureAttemptModel(deps.db, deps.tables);\n const logs = new CaptureLogModel(deps.db, deps.tables);\n const attempt = await attempts.startAttempt(project.id, build.id, reqId);\n const recordLog: AttemptLogRecorder = async (level, message, fields) => {\n await logs.append(project.id, build.id, attempt.id, level, message, fields);\n };\n await emitAttemptLog(recordLog, deps.logger, \"info\", \"capture queued\", {\n buildId: build.id,\n attemptNo: attempt.attemptNo,\n });\n return { attempt, recordLog };\n}\n\n/** Close the attempt row with its terminal status, counts, and error. */\nasync function finishAttempt(\n ctx: JobContext,\n status: \"completed\" | \"failed\" | \"cancelled\",\n error?: string,\n counts?: { storyCount: number; failedCount: number },\n): Promise<void> {\n const { deps, attempt } = ctx;\n await new CaptureAttemptModel(deps.db, deps.tables).markFinished(\n attempt.id,\n status,\n error,\n counts,\n );\n await emitAttemptLog(\n ctx.recordLog,\n deps.logger,\n status === \"failed\" ? \"error\" : \"info\",\n `capture ${status}`,\n { attemptNo: attempt.attemptNo },\n );\n}\n\n/** Full error text for an attempt row (message plus stack when available). */\nfunction errorText(reason: unknown): string {\n if (reason instanceof Error) {\n return reason.stack ?? reason.message;\n }\n return String(reason);\n}\n\nasync function postPending(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project,\n sha,\n status: \"pending\",\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post pending status\");\n });\n}\n\nasync function postTerminal(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n status: \"success\" | \"failure\",\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project,\n sha,\n status,\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post terminal status\");\n });\n}\n\nasync function approveWithoutCapture(ctx: JobContext, reason: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n deps.logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, reason);\n await emitAttemptLog(ctx.recordLog, deps.logger, \"info\", reason, {\n sha: build.gitSha,\n branch: build.gitBranch,\n });\n await finishAttempt(ctx, \"completed\");\n await new BuildModel(deps.db, deps.tables).setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postTerminal(deps, project, pendingUrl, build.gitSha, \"success\").catch(() => {}); // Intentionally empty — fire-and-forget\n}\n\nasync function maybeSkipMerged(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n if (build.isDefault) {\n return false;\n }\n const merged = await isAlreadyMerged({\n providers: deps.gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: deps.secret,\n db: deps.db,\n tables: deps.tables,\n projectId: project.id,\n logger: deps.logger,\n }).catch(() => false);\n if (merged) {\n await approveWithoutCapture(ctx, \"skipping capture — already merged\");\n return true;\n }\n return false;\n}\n\nasync function maybeSkipDuplicate(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n const dup = await hasApprovedBuildForSha(\n deps.db,\n deps.tables,\n project.id,\n build.gitSha,\n build.id,\n );\n if (dup) {\n await approveWithoutCapture(ctx, \"skipping capture — duplicate sha already approved\");\n return true;\n }\n return false;\n}\n\n/** Map a terminal build status to its check-run state. */\nfunction terminalState(status: string | undefined): \"success\" | \"failure\" | null {\n if (status === \"approved\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"failure\";\n }\n return null;\n}\n\n/** Post the terminal status for a finished capture. */\nasync function reportTerminalStatus(ctx: JobContext, buildId: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n const updated = await new BuildModel(deps.db, deps.tables).get(buildId);\n const terminal = terminalState(updated?.status);\n if (terminal) {\n await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);\n }\n}\n\nasync function runAndReport(ctx: JobContext, job: CaptureDispatchJob): Promise<void> {\n const { deps, build } = ctx;\n await new CaptureAttemptModel(deps.db, deps.tables).markRunning(ctx.attempt.id);\n await emitAttemptLog(ctx.recordLog, deps.logger, \"info\", \"capture started\", {\n buildId: build.id,\n attemptNo: ctx.attempt.attemptNo,\n });\n try {\n const summary = await executeCaptureJob(\n {\n buildId: job.buildId,\n reqId: job.reqId,\n traceparent: job.traceparent,\n attempt: {\n id: ctx.attempt.id,\n attemptNo: ctx.attempt.attemptNo,\n projectId: ctx.project.id,\n },\n recordLog: ctx.recordLog,\n },\n deps.jobOptions,\n );\n await finishAttempt(ctx, \"completed\", undefined, summary);\n await reportTerminalStatus(ctx, job.buildId);\n } catch (error: unknown) {\n await finishAttempt(ctx, \"failed\", errorText(error));\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project: ctx.project,\n sha: build.gitSha,\n status: \"failure\",\n url: ctx.pendingUrl,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n}\n\n/** Load the build + project for a job, running orphan jobs directly. */\nasync function loadJobTarget(\n deps: DispatchDeps,\n job: CaptureDispatchJob,\n): Promise<{ build: Build; project: Project } | null> {\n const builds = new BuildModel(deps.db, deps.tables);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob(\n { buildId: job.buildId, reqId: job.reqId, traceparent: job.traceparent },\n deps.jobOptions,\n );\n return null;\n }\n const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);\n if (!project) {\n await executeCaptureJob(\n { buildId: job.buildId, reqId: job.reqId, traceparent: job.traceparent },\n deps.jobOptions,\n );\n return null;\n }\n return { build, project };\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/capture/guards.ts
5
+ /**
6
+ * Capture output guards: a third-party runner returning garbage (or
7
+ * gigabytes) must fail here, before bytes reach storage or baselines.
8
+ */
9
+ /** Largest screenshot buffer the pipeline accepts (25 MiB). */
10
+ const MAX_SCREENSHOT_BYTES = 26214400;
11
+ /** PNG magic bytes every screenshot must start with. */
12
+ const PNG_MAGIC = [
13
+ 137,
14
+ 80,
15
+ 78,
16
+ 71,
17
+ 13,
18
+ 10,
19
+ 26,
20
+ 10
21
+ ];
22
+ /** Throw when a runner-produced screenshot is unusable or unsafe to store. */
23
+ function assertScreenshotBuffer(buffer) {
24
+ if (buffer.length === 0) throw new Error("Screenshot is empty");
25
+ if (buffer.length > 26214400) throw new Error(`Screenshot exceeds ${MAX_SCREENSHOT_BYTES} bytes`);
26
+ if (!PNG_MAGIC.every((byte, index) => buffer[index] === byte)) throw new Error("Screenshot is not a PNG");
27
+ }
28
+ //#endregion
29
+ export { MAX_SCREENSHOT_BYTES, assertScreenshotBuffer };
30
+
31
+ //# sourceMappingURL=guards.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.mjs","names":[],"sources":["../../src/capture/guards.ts"],"sourcesContent":["/**\n * Capture output guards: a third-party runner returning garbage (or\n * gigabytes) must fail here, before bytes reach storage or baselines.\n */\n\n/** Largest screenshot buffer the pipeline accepts (25 MiB). */\nexport const MAX_SCREENSHOT_BYTES = 25 * 1024 * 1024;\n\n/** PNG magic bytes every screenshot must start with. */\nconst PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\n/** Throw when a runner-produced screenshot is unusable or unsafe to store. */\nexport function assertScreenshotBuffer(buffer: Buffer): void {\n if (buffer.length === 0) {\n throw new Error(\"Screenshot is empty\");\n }\n if (buffer.length > MAX_SCREENSHOT_BYTES) {\n throw new Error(`Screenshot exceeds ${MAX_SCREENSHOT_BYTES} bytes`);\n }\n const magic = PNG_MAGIC.every((byte, index) => buffer[index] === byte);\n if (!magic) {\n throw new Error(\"Screenshot is not a PNG\");\n }\n}\n"],"mappings":";;;;;;;;;AAMA,MAAa,uBAAuB;;AAGpC,MAAM,YAAY;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;;AAGjE,SAAgB,uBAAuB,QAAsB;CAC3D,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,qBAAqB;CAEvC,IAAI,OAAO,SAAA,UACT,MAAM,IAAI,MAAM,sBAAsB,qBAAqB,OAAO;CAGpE,IAAI,CADU,UAAU,OAAO,MAAM,UAAU,OAAO,WAAW,IACxD,GACP,MAAM,IAAI,MAAM,yBAAyB;AAE7C"}
@@ -4,29 +4,64 @@ import { Viewport } from "./adapter.mjs";
4
4
  import { CaptureRunner } from "../adapter/capture-runner.mjs";
5
5
  import { DatabaseAdapter } from "../adapter/database.mjs";
6
6
  import { StorageAdapter } from "../adapter/storage.mjs";
7
+ import { SecretInput } from "../utils/encrypt.mjs";
8
+ import { BuildTables } from "../models/build.mjs";
9
+ import { AttemptLogRecorder, CaptureAttemptTables } from "../models/capture-attempt.mjs";
10
+ import { CaptureLogTables } from "../models/capture-log.mjs";
11
+ import { ProjectTables } from "../models/project.mjs";
12
+ import { PipelineTables } from "./pipeline.mjs";
7
13
  import { Logger } from "pino";
8
14
  //#region src/capture/orchestrator.d.ts
15
+ /** Table handles required by the orchestrator. */
16
+ type OrchestratorTables = BuildTables & ProjectTables & PipelineTables & CaptureAttemptTables & CaptureLogTables;
9
17
  /** Inputs for running a capture job against a Storybook build. */
10
18
  interface CaptureJobOptions {
11
19
  db: DatabaseAdapter;
20
+ tables: OrchestratorTables;
12
21
  storage: StorageAdapter;
13
22
  runner: CaptureRunner;
14
23
  scratchDir: string;
15
24
  viewports?: Viewport[];
16
25
  logger?: Logger;
17
26
  /** Server secret for decrypting webhook secrets at send time. */
18
- secret?: string | undefined;
27
+ secret?: SecretInput;
28
+ /** Render budget override (tests use a short fuse). */
29
+ renderTimeoutMs?: number | undefined;
30
+ }
31
+ /** Pointer to the attempt row owning this run's log history. */
32
+ interface CaptureAttemptRef {
33
+ id: string;
34
+ attemptNo: number;
35
+ projectId: string;
36
+ }
37
+ /** Inputs for running a capture job against a Storybook build. */
38
+ interface CaptureJobInput {
39
+ buildId: string;
40
+ reqId?: string;
41
+ /**
42
+ * W3C `traceparent` from the enqueueing request or queue payload.
43
+ * Continues the inbound trace instead of starting a root span.
44
+ */
45
+ traceparent?: string;
46
+ /** Attempt row owning this run; when omitted no per-attempt logs are stored. */
47
+ attempt?: CaptureAttemptRef;
48
+ /** Recorder mirroring phase logs into the attempt's log history. */
49
+ recordLog?: AttemptLogRecorder;
19
50
  }
20
51
  /**
21
52
  * Run the full capture for a build: extract, render, persist, and finalize.
22
53
  *
54
+ * The run executes inside a `capture.job` span (continuing `input.traceparent`
55
+ * when present) with one child span per pipeline phase.
56
+ *
23
57
  * @param input - Build id plus the originating request id.
24
58
  * @param options - Adapters, scratch dir, viewports, and logger.
59
+ * @returns Story and blocking-failure counts for the attempt row.
25
60
  */
26
- declare function executeCaptureJob(input: {
27
- buildId: string;
28
- reqId?: string;
29
- }, options: CaptureJobOptions): Promise<void>;
61
+ declare function executeCaptureJob(input: CaptureJobInput, options: CaptureJobOptions): Promise<{
62
+ storyCount: number;
63
+ failedCount: number;
64
+ }>;
30
65
  //#endregion
31
- export { CaptureJobOptions, executeCaptureJob };
66
+ export { CaptureAttemptRef, CaptureJobInput, CaptureJobOptions, OrchestratorTables, executeCaptureJob };
32
67
  //# sourceMappingURL=orchestrator.d.mts.map
@@ -1,90 +1,213 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from "./adapter.mjs";
4
5
  import { BuildModel } from "../models/build.mjs";
6
+ import { emitAttemptLog } from "../models/capture-attempt.mjs";
5
7
  import { ProjectModel } from "../models/project.mjs";
6
- import { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from "./adapter.mjs";
8
+ import { parentContext, withSpan } from "../tracing.mjs";
9
+ import { roundedTimings, runWithTimings, timed } from "../utils/timing.mjs";
10
+ import { StorybookAdapter } from "./storybook.mjs";
11
+ import { partitionAffectedStories } from "./affected.mjs";
12
+ import { resolveViewports } from "./sizing.mjs";
7
13
  import { persistCapture } from "./pipeline.mjs";
8
14
  import { extractStorybookToScratch, persistStorybookStatics } from "./statics.mjs";
9
- import { StorybookAdapter } from "./storybook.mjs";
10
15
  //#region src/capture/orchestrator.ts
16
+ /** Default budget for one render call before the orchestrator cancels it. */
17
+ const RENDER_TIMEOUT_MS = 9e5;
18
+ /** Failure error text cap (persisted rows and logs stay bounded). */
19
+ const FAILURE_ERROR_MAX = 500;
20
+ /** Cap failure error text so one chatty runner cannot flood rows and logs. */
21
+ function capFailures(result) {
22
+ return {
23
+ captures: result.captures,
24
+ failures: result.failures.map((failure) => ({
25
+ ...failure,
26
+ error: failure.error.length > FAILURE_ERROR_MAX ? failure.error.slice(0, FAILURE_ERROR_MAX) : failure.error
27
+ }))
28
+ };
29
+ }
30
+ /** Render with a timeout; cancels the runner when the budget expires. */
31
+ async function renderWithTimeout(runner, request, timeoutMs) {
32
+ let timedOut = false;
33
+ let timer;
34
+ const timeout = new Promise((_, reject) => {
35
+ timer = setTimeout(() => {
36
+ timedOut = true;
37
+ reject(/* @__PURE__ */ new Error(`Render timed out after ${timeoutMs}ms`));
38
+ }, timeoutMs);
39
+ });
40
+ timer?.unref?.();
41
+ try {
42
+ return capFailures(await Promise.race([runner.render(request), timeout]));
43
+ } catch (error) {
44
+ if (timedOut) await runner.cancel(request.buildId).catch(() => {});
45
+ throw error;
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
11
50
  /**
12
51
  * Run the full capture for a build: extract, render, persist, and finalize.
13
52
  *
53
+ * The run executes inside a `capture.job` span (continuing `input.traceparent`
54
+ * when present) with one child span per pipeline phase.
55
+ *
14
56
  * @param input - Build id plus the originating request id.
15
57
  * @param options - Adapters, scratch dir, viewports, and logger.
58
+ * @returns Story and blocking-failure counts for the attempt row.
16
59
  */
17
60
  async function executeCaptureJob(input, options) {
18
- const builds = new BuildModel(options.db);
61
+ const builds = new BuildModel(options.db, options.tables);
19
62
  const { build, project } = await loadTarget(options, input.buildId);
63
+ return await withSpan("capture.job", async () => {
64
+ const { result, timings } = await runWithTimings(async () => await runCapturePhases(input, options, builds, build, project));
65
+ options.logger?.child({
66
+ buildId: build.id,
67
+ ...input.reqId ? { reqId: input.reqId } : {}
68
+ })?.info({ timings: roundedTimings(timings) }, "capture timings");
69
+ return result;
70
+ }, {
71
+ "storyshelf.build_id": build.id,
72
+ ...input.reqId ? { "storyshelf.req_id": input.reqId } : {},
73
+ ...input.attempt ? { "storyshelf.attempt_no": input.attempt.attemptNo } : {}
74
+ }, parentContext(input.traceparent));
75
+ }
76
+ /** Execute the phase pipeline inside the `capture.job` span. */
77
+ async function runCapturePhases(input, options, builds, build, project) {
20
78
  const logger = options.logger?.child({
21
79
  buildId: input.buildId,
22
- reqId: input.reqId
80
+ reqId: input.reqId,
81
+ ...input.attempt ? { attemptNo: input.attempt.attemptNo } : {}
23
82
  });
24
83
  await builds.setStatus(build.id, "capturing");
25
84
  const startTime = performance.now();
26
85
  let extractedDir;
27
86
  try {
28
87
  const extractStart = performance.now();
29
- extractedDir = await extractStorybookToScratch(options.storage, options.scratchDir, project.id, build.id);
88
+ extractedDir = await timed("capture.extract", async () => await withSpan("capture.extract", async () => {
89
+ return await extractStorybookToScratch(options.storage, options.scratchDir, project.id, build.id);
90
+ }));
30
91
  const extractDuration = performance.now() - extractStart;
31
92
  logger?.info({ durationMs: Math.round(extractDuration) }, "storybook extracted");
93
+ await emitAttemptLog(input.recordLog, logger, "info", "storybook extracted", { durationMs: Math.round(extractDuration) });
94
+ const storybookDir = extractedDir;
32
95
  const staticsStart = performance.now();
33
- await persistStorybookStatics(options.storage, extractedDir, project.id, build.id);
96
+ await timed("capture.persist-statics", async () => await withSpan("capture.persist-statics", async () => {
97
+ await persistStorybookStatics(options.storage, storybookDir, project.id, build.id);
98
+ }));
34
99
  logger?.info({ durationMs: Math.round(performance.now() - staticsStart) }, "storybook statics persisted");
100
+ await emitAttemptLog(input.recordLog, logger, "info", "storybook statics persisted", { durationMs: Math.round(performance.now() - staticsStart) });
35
101
  const stories = (await new StorybookAdapter().discover(extractedDir)).filter((s) => !isDisabledStory(s));
36
- const viewports = options.viewports ?? DEFAULT_VIEWPORTS;
37
- const renderStart = performance.now();
38
- const result = await options.runner.render({
39
- buildId: build.id,
40
- storybookDir: extractedDir,
102
+ const rawViewports = project.viewports;
103
+ const viewports = resolveViewports(rawViewports, DEFAULT_VIEWPORTS, options.viewports);
104
+ const browser = project.browser;
105
+ const partition = await partitionAffectedStories({
106
+ db: options.db,
107
+ tables: options.tables,
108
+ projectId: project.id,
109
+ branch: build.gitBranch,
110
+ defaultBranch: project.gitDefaultBranch,
41
111
  stories,
42
112
  viewports,
43
- logger,
44
- executePlay: project.executePlay ?? false,
45
- playTimeoutMs: project.playTimeoutMs ?? 1e4
113
+ affectedPaths: affectedPathsFor(build)
46
114
  });
115
+ logger?.info({
116
+ rendered: partition.render.length,
117
+ inherited: partition.inherited.length,
118
+ total: stories.length
119
+ }, "affected capture partitioned");
120
+ await emitAttemptLog(input.recordLog, logger, "info", "affected capture partitioned", {
121
+ rendered: partition.render.length,
122
+ inherited: partition.inherited.length,
123
+ total: stories.length
124
+ });
125
+ const renderStart = performance.now();
126
+ const renderTimeoutMs = options.renderTimeoutMs ?? 9e5;
127
+ const result = await timed("capture.render", async () => await withSpan("capture.render", async () => {
128
+ if (partition.render.length === 0) return {
129
+ captures: [],
130
+ failures: []
131
+ };
132
+ return await renderWithTimeout(options.runner, {
133
+ buildId: build.id,
134
+ storybookDir,
135
+ stories: partition.render,
136
+ viewports,
137
+ logger,
138
+ executePlay: project.executePlay ?? false,
139
+ playTimeoutMs: project.playTimeoutMs ?? 1e4,
140
+ runA11y: project.runA11y ?? false,
141
+ browser: browser ?? "chromium"
142
+ }, renderTimeoutMs);
143
+ }, { "storyshelf.render_count": partition.render.length }));
47
144
  const renderDuration = performance.now() - renderStart;
48
145
  logger?.info({
49
146
  durationMs: Math.round(renderDuration),
50
147
  storyCount: stories.length
51
148
  }, "stories rendered");
149
+ await emitAttemptLog(input.recordLog, logger, "info", "stories rendered", {
150
+ durationMs: Math.round(renderDuration),
151
+ storyCount: stories.length
152
+ });
52
153
  const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));
53
154
  const blockingFailed = /* @__PURE__ */ new Set();
54
155
  const flakyFailed = /* @__PURE__ */ new Set();
55
- for (const f of result.failures) if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);
156
+ const a11yFailed = /* @__PURE__ */ new Set();
157
+ for (const f of result.failures) if (f.error.startsWith("a11y:")) a11yFailed.add(f.storyId);
158
+ else if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);
56
159
  else blockingFailed.add(f.storyId);
57
160
  const persistStart = performance.now();
58
- await persistCapture({
59
- db: options.db,
60
- storage: options.storage,
61
- project,
62
- build,
63
- viewports,
64
- captures: result.captures,
65
- logger,
66
- secret: options.secret
67
- }, blockingFailed, flakyFailed);
161
+ await timed("capture.persist", async () => await withSpan("capture.persist", async () => {
162
+ await persistCapture({
163
+ db: options.db,
164
+ tables: options.tables,
165
+ storage: options.storage,
166
+ project,
167
+ build,
168
+ viewports,
169
+ captures: result.captures,
170
+ inherited: partition.inherited,
171
+ logger,
172
+ recordLog: input.recordLog,
173
+ secret: options.secret
174
+ }, blockingFailed, flakyFailed, a11yFailed);
175
+ }));
68
176
  const persistDuration = performance.now() - persistStart;
69
177
  logger?.info({ durationMs: Math.round(persistDuration) }, "capture persisted");
178
+ await emitAttemptLog(input.recordLog, logger, "info", "capture persisted", { durationMs: Math.round(persistDuration) });
70
179
  const totalDuration = performance.now() - startTime;
71
180
  logger?.info({ durationMs: Math.round(totalDuration) }, "capture completed");
181
+ await emitAttemptLog(input.recordLog, logger, "info", "capture completed", { durationMs: Math.round(totalDuration) });
182
+ return {
183
+ storyCount: stories.length,
184
+ failedCount: blockingFailed.size
185
+ };
72
186
  } catch (error) {
73
187
  const totalDuration = performance.now() - startTime;
74
188
  logger?.error({
75
189
  durationMs: Math.round(totalDuration),
76
190
  err: error
77
191
  }, "capture failed");
192
+ await emitAttemptLog(input.recordLog, logger, "error", "capture failed", { durationMs: Math.round(totalDuration) });
78
193
  await builds.setStatus(build.id, "failed").catch((markError) => {
79
194
  logger?.error({ err: markError }, "failed to mark build failed after capture error");
80
195
  });
81
196
  throw error;
82
197
  }
83
198
  }
199
+ /**
200
+ * Resolve the affected import paths for a build, or `null` for full capture
201
+ * (affected capture disabled, or no computation was posted).
202
+ */
203
+ function affectedPathsFor(build) {
204
+ if (!build.affectedOnly) return null;
205
+ return BuildModel.affectedPaths(build);
206
+ }
84
207
  async function loadTarget(options, buildId) {
85
- const build = await new BuildModel(options.db).get(buildId);
208
+ const build = await new BuildModel(options.db, options.tables).get(buildId);
86
209
  if (!build) throw new Error(`Build not found: ${buildId}`);
87
- const project = await new ProjectModel(options.db).get(build.projectId);
210
+ const project = await new ProjectModel(options.db, options.tables).get(build.projectId);
88
211
  if (!project) throw new Error(`Project not found: ${build.projectId}`);
89
212
  return {
90
213
  build,
@@ -92,6 +215,6 @@ async function loadTarget(options, buildId) {
92
215
  };
93
216
  }
94
217
  //#endregion
95
- export { executeCaptureJob };
218
+ export { RENDER_TIMEOUT_MS, executeCaptureJob };
96
219
 
97
220
  //# sourceMappingURL=orchestrator.mjs.map