canopycms 0.0.68-int.98 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -1,15 +1,19 @@
1
1
  import fs from 'node:fs/promises';
2
- import { simpleGit } from 'simple-git';
3
- import { completeTask, dequeueTask, failTask, recoverOrphanedTasks, retryTask, } from '../task-queue/cms-task-queue.js';
4
- import { createOrUpdatePullRequest } from '../github-service.js';
5
- import { BranchMetadataFileManager, getBranchMetadataFileManager } from '../branch-metadata.js';
2
+ import { completeTask, dequeueTask, failTask, recoverOrphanedTasks, releaseTask, retryTask, } from '../task-queue/cms-task-queue.js';
3
+ import { isNoCommitsBetweenError, isRefAlreadyGoneError } from '../github-service.js';
4
+ import { BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, } from '../branch-metadata.js';
6
5
  import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/branch-name.js';
7
- import { gitNetworkChildEnv } from '../git-manager.js';
8
6
  import { getErrorMessage, redactCredentials } from '../utils/error.js';
9
- import { isNonFastForwardRejection, isStaleLeaseRejection, workflowPushRefusalFile, } from '../utils/git.js';
7
+ import { isNonFastForwardRejection, workflowPushRefusalFile } from '../utils/git.js';
8
+ import { GitHubPushError, describeMove } from './github-gateway.js';
9
+ import { GatewayRefusalError } from './branch-policy.js';
10
+ import { GatewayRequestRefusedError, GitHubGatewayStoppingError, GitHubGatewayUnreachableError, } from './github-gateway-wire.js';
11
+ import { RefusedPushError, assertPlainBranchName } from './github-mirror.js';
12
+ import { pushToGitHub } from './github-transfer.js';
10
13
  import { clearHistoryRewrittenMarker, readPublishedSha } from './history-rewrite.js';
11
14
  import { writeWorkerStatus } from '../task-queue/worker-status.js';
12
- import { workerLog, workerLogError } from './log.js';
15
+ import { workerLog, workerLogError, workerLogWarn } from './log.js';
16
+ import { assertSharedRepoConfig } from './shared-repo-git.js';
13
17
  /**
14
18
  * An error inherent to the task itself (malformed payload, unknown action):
15
19
  * retrying can never succeed, so the task should fail fast instead of
@@ -17,6 +21,19 @@ import { workerLog, workerLogError } from './log.js';
17
21
  */
18
22
  export class PermanentTaskError extends Error {
19
23
  }
24
+ /**
25
+ * The attempt was cut off by a draining `stop()` reaching its deadline, not by
26
+ * anything wrong with the task: it goes back to pending with no retry spent.
27
+ */
28
+ class TaskAbortedForShutdownError extends Error {
29
+ }
30
+ /**
31
+ * GitHub refused a submit's PR because the pushed branch has no commits its base lacks: the API's
32
+ * own check found changes against an older base, or could not run. The failure handler returns
33
+ * the branch to editing if it is still in that submit, since nothing is under review.
34
+ */
35
+ class NothingToSubmitTaskError extends PermanentTaskError {
36
+ }
20
37
  /**
21
38
  * Classify a task failure as permanent (fail fast) or transient (retry).
22
39
  *
@@ -35,12 +52,17 @@ export class PermanentTaskError extends Error {
35
52
  *
36
53
  * Permanent — retrying the identical request cannot succeed:
37
54
  * - PermanentTaskError (malformed payload, unknown action);
55
+ * - GatewayRefusalError: the GitHub gateway's policy refused it (worker/branch-policy.ts);
56
+ * - GatewayRequestRefusedError: the gateway's socket refused it for its shape or size;
38
57
  * - other HTTP 4xx (401/404/422): the request itself is bad;
39
58
  * - plain HTTP 403 with no rate-limit signal: a real permission denial.
40
59
  */
41
60
  export function isPermanentTaskFailure(err) {
42
- if (err instanceof PermanentTaskError)
61
+ if (err instanceof PermanentTaskError ||
62
+ err instanceof GatewayRefusalError ||
63
+ err instanceof GatewayRequestRefusedError) {
43
64
  return true;
65
+ }
44
66
  const status = getHttpStatus(err);
45
67
  if (status === null)
46
68
  return false;
@@ -151,7 +173,10 @@ export async function processTaskQueue(ctx) {
151
173
  }
152
174
  let processed = 0;
153
175
  let task;
176
+ // Checked before every claim: a draining worker finishes the task it holds
177
+ // and claims no other.
154
178
  while (processed < ctx.maxTasksPerCycle &&
179
+ !ctx.isDraining() &&
155
180
  (task = await dequeueTask(ctx.taskDir, ctx.log)) !== null) {
156
181
  try {
157
182
  const result = await executeTaskWithTimeout(ctx, task);
@@ -159,12 +184,27 @@ export async function processTaskQueue(ctx) {
159
184
  await updateBranchMetadata(ctx, task, result);
160
185
  }
161
186
  catch (err) {
187
+ // A stop, not the task, is why these failed, so the task goes back to pending with no retry
188
+ // spent, as after a drain's abort: the gateway draining, or out of reach while this worker
189
+ // drains (its host going away).
190
+ const gatewayStopping = err instanceof GitHubGatewayStoppingError;
191
+ const gatewayGone = ctx.isDraining() && err instanceof GitHubGatewayUnreachableError;
192
+ if (err instanceof TaskAbortedForShutdownError || gatewayStopping || gatewayGone) {
193
+ // It runs again from the start, exactly as a retry after a timed-out attempt does.
194
+ await releaseTask(ctx.taskDir, task.id, ctx.log);
195
+ workerLogWarn(gatewayStopping
196
+ ? `The GitHub gateway is stopping; released task ${task.id} (${task.action}) to pending`
197
+ : gatewayGone
198
+ ? `The GitHub gateway is out of reach while the worker drains; released task ${task.id} (${task.action}) to pending for the next worker`
199
+ : `Drain deadline hit, aborted task ${task.id} (${task.action}); released to pending for the next worker`);
200
+ break;
201
+ }
162
202
  const message = getErrorMessage(err);
163
203
  workerLogError(`Task ${task.id} (${task.action}) failed:`, message);
164
204
  // [REDACT] task.error is persisted (pending/failed task JSON) and served
165
- // to the browser by the admin panel's Tasks tab -- a push failure's
166
- // message can embed the bot token via buildGitHubUrl(). Console output
167
- // above stays raw (journald/CloudWatch is trusted).
205
+ // to the browser by the admin panel's Tasks tab, so a credential in any
206
+ // failure's message must not reach it. Console output above stays raw
207
+ // (journald/CloudWatch is trusted).
168
208
  const persistedMessage = redactCredentials(message);
169
209
  // DEP-L1: only transient failures are worth retrying; a permanent one
170
210
  // would burn the retry budget on an identical doomed request.
@@ -177,21 +217,13 @@ export async function processTaskQueue(ctx) {
177
217
  }
178
218
  else {
179
219
  await failTask(ctx.taskDir, task.id, persistedMessage, ctx.log);
180
- await updateBranchMetadataOnFailure(ctx, task, persistedMessage);
220
+ await updateBranchMetadataOnFailure(ctx, task, persistedMessage, {
221
+ unlock: err instanceof NothingToSubmitTaskError,
222
+ });
181
223
  workerLogError(permanent
182
224
  ? ' Permanently failed (non-retryable error)'
183
225
  : ` Permanently failed after ${maxRetries} retries`);
184
226
  }
185
- // The credential may have rotated, and the next retry resolves
186
- // buildGitHubUrl() afresh to pick a new token up. Without this a push
187
- // meeting a revoked token spends its whole retry budget (5s/10s/20s
188
- // backoff) waiting on the git-sync loop's refresh up to 5 minutes away,
189
- // and fails permanently while the working token sits in the secret store.
190
- //
191
- // Ungated, and AFTER the outcome is recorded rather than before: the task
192
- // is safely in pending/ or failed/ while a network read runs, and that
193
- // read is bounded and never throws. See CmsWorker.refreshGitHubCredential.
194
- await ctx.refreshGitHubCredential();
195
227
  }
196
228
  processed++;
197
229
  }
@@ -206,31 +238,38 @@ export async function processTaskQueue(ctx) {
206
238
  }
207
239
  }
208
240
  /**
209
- * Execute a task bounded by taskTimeoutMs (DEP-H1). Two layers:
210
- * - an AbortSignal cancels Octokit HTTP calls promptly;
211
- * - a Promise.race rejects when the timeout fires, so work that cannot observe
212
- * the signal (git subprocesses via simple-git, and a hung
213
- * `ctx.buildGitHubUrl()` in pushBranchToGitHub) still fails the attempt and
214
- * the worker moves on instead of stalling forever.
241
+ * Execute a task bounded by taskTimeoutMs (DEP-H1) and by the worker's
242
+ * shutdown signal. Two layers:
243
+ * - an AbortSignal cancels Octokit HTTP calls and kills the push's git process
244
+ * (pushBranchToGitHub passes it to simple-git's `abort`);
245
+ * - a Promise.race rejects when either fires, so work that cannot observe the
246
+ * signal (a hung credential resolution in the gateway's push) still ends
247
+ * the attempt and the worker moves on instead of stalling forever.
215
248
  *
216
- * A raced-out resolution is not CANCELLED: if it later settles, the abandoned
217
- * push continues. On the App path the mint is separately bounded by
218
- * gitTokenMintTimeoutMs, and git-sync.ts's two resolutions run on the sync loop
219
- * and are not bounded by taskTimeoutMs at all. pushBranchToGitHub additionally
220
- * kills stalled git processes via simple-git's block timeout.
249
+ * A raced-out credential resolution is not cancelled. On the App path the mint
250
+ * is separately bounded by gitTokenMintTimeoutMs, and git-sync.ts's two
251
+ * resolutions run on the sync loop and are not bounded by taskTimeoutMs at all.
221
252
  */
222
253
  async function executeTaskWithTimeout(ctx, task) {
223
254
  const controller = new AbortController();
224
255
  const timer = setTimeout(() => controller.abort(), ctx.taskTimeoutMs);
256
+ const shutdown = ctx.shutdownSignal();
257
+ const signal = AbortSignal.any([controller.signal, shutdown]);
225
258
  try {
226
- const work = ctx.executeTask(task, controller.signal);
227
- // If the timeout wins the race, the losing promise must not surface an
259
+ const work = ctx.executeTask(task, signal);
260
+ // If the abort wins the race, the losing promise must not surface an
228
261
  // unhandled rejection when it eventually settles.
229
262
  work.catch(() => { });
230
- const timedOut = new Promise((_, reject) => {
231
- controller.signal.addEventListener('abort', () => reject(new Error(`Task timed out after ${ctx.taskTimeoutMs}ms`)), { once: true });
263
+ const aborted = new Promise((_, reject) => {
264
+ const onAbort = () => reject(shutdown.aborted
265
+ ? new TaskAbortedForShutdownError(`Task aborted: the worker is shutting down`)
266
+ : new Error(`Task timed out after ${ctx.taskTimeoutMs}ms`));
267
+ if (signal.aborted)
268
+ onAbort();
269
+ else
270
+ signal.addEventListener('abort', onAbort, { once: true });
232
271
  });
233
- return await Promise.race([work, timedOut]);
272
+ return await Promise.race([work, aborted]);
234
273
  }
235
274
  finally {
236
275
  clearTimeout(timer);
@@ -241,36 +280,29 @@ export async function executeTask(ctx, task, signal) {
241
280
  switch (action) {
242
281
  case 'push-branch': {
243
282
  const branch = requireString(payload, 'branch');
244
- await ctx.pushBranchToGitHub(branch);
283
+ await ctx.pushBranchToGitHub(branch, signal);
245
284
  return { pushed: true };
246
285
  }
247
286
  case 'push-and-create-pr': {
248
287
  const branch = requireString(payload, 'branch');
249
- await ctx.pushBranchToGitHub(branch);
250
- const pr = await ctx.octokit().pulls.create({
251
- owner: ctx.githubOwner,
252
- repo: ctx.githubRepo,
288
+ await ctx.pushBranchToGitHub(branch, signal);
289
+ const pr = await ctx.github().createPullRequest({
253
290
  head: branch,
254
291
  base: optionalString(payload, 'baseBranch', ctx.baseBranch),
255
292
  title: optionalString(payload, 'title', `Submit ${branch}`),
256
293
  body: optionalString(payload, 'body', ''),
257
- request: { signal },
258
- });
259
- workerLog(`Created PR #${pr.data.number} for ${branch}`);
260
- return { prUrl: pr.data.html_url, prNumber: pr.data.number };
294
+ }, signal);
295
+ workerLog(`Created PR #${pr.number} for ${branch}`);
296
+ return { prUrl: pr.url, prNumber: pr.number };
261
297
  }
262
298
  case 'push-and-update-pr': {
263
299
  const branch = requireString(payload, 'branch');
264
300
  const prNumber = requireNumber(payload, 'pullRequestNumber');
265
- await ctx.pushBranchToGitHub(branch);
266
- await ctx.octokit().pulls.update({
267
- owner: ctx.githubOwner,
268
- repo: ctx.githubRepo,
269
- pull_number: prNumber,
301
+ await ctx.pushBranchToGitHub(branch, signal);
302
+ await ctx.github().updatePullRequest(prNumber, {
270
303
  title: optionalString(payload, 'title', `Submit ${branch}`),
271
304
  body: optionalString(payload, 'body', ''),
272
- request: { signal },
273
- });
305
+ }, signal);
274
306
  workerLog(`Updated PR #${prNumber} for ${branch}`);
275
307
  return { prNumber };
276
308
  }
@@ -294,24 +326,29 @@ export async function executeTask(ctx, task, signal) {
294
326
  // The settings branch is an orphan with no history in common with the
295
327
  // base, so GitHub 422s a PR for it: push it and stop.
296
328
  if (isSettingsBranch(branch)) {
297
- await ctx.pushBranchToGitHub(branch);
298
- workerLog(`Pushed settings branch ${branch}; settings branches never get a PR`);
329
+ await ctx.pushBranchToGitHub(branch, signal);
330
+ workerLog(`No PR for ${branch}: settings branches never get one`);
299
331
  return { pushed: true };
300
332
  }
301
- await ctx.pushBranchToGitHub(branch);
302
- const result = await createOrUpdatePullRequest({
303
- octokit: ctx.octokit(),
304
- owner: ctx.githubOwner,
305
- repo: ctx.githubRepo,
306
- head: branch,
307
- base,
308
- title: optionalString(payload, 'title', `Submit ${branch}`),
309
- body: optionalString(payload, 'body', ''),
310
- // Content submits (api/github-sync.ts) set both.
311
- markReadyIfDraft: payload.markReadyIfDraft === true,
312
- mergeSectionIntoBody: payload.mergeSectionIntoBody === true,
313
- signal,
314
- });
333
+ await ctx.pushBranchToGitHub(branch, signal);
334
+ let result;
335
+ try {
336
+ result = await ctx.github().createOrUpdatePullRequest({
337
+ head: branch,
338
+ base,
339
+ title: optionalString(payload, 'title', `Submit ${branch}`),
340
+ body: optionalString(payload, 'body', ''),
341
+ // Content submits (api/github-sync.ts) set both.
342
+ markReadyIfDraft: payload.markReadyIfDraft === true,
343
+ mergeSectionIntoBody: payload.mergeSectionIntoBody === true,
344
+ }, signal);
345
+ }
346
+ catch (err) {
347
+ if (!isNoCommitsBetweenError(err))
348
+ throw err;
349
+ throw new NothingToSubmitTaskError(`Nothing was submitted: "${branch}" has no changes compared with "${base}", so GitHub ` +
350
+ 'opened no pull request.');
351
+ }
315
352
  workerLog(result.created
316
353
  ? `Created PR #${result.number} for ${branch}`
317
354
  : `Updated existing PR #${result.number} for ${branch}`);
@@ -319,50 +356,83 @@ export async function executeTask(ctx, task, signal) {
319
356
  }
320
357
  case 'convert-to-draft': {
321
358
  const draftPrNumber = requireNumber(payload, 'pullRequestNumber');
322
- // GitHub REST API doesn't support converting to draft directly.
323
- // Use the GraphQL API via Octokit.
324
- const { data: pr } = await ctx.octokit().pulls.get({
325
- owner: ctx.githubOwner,
326
- repo: ctx.githubRepo,
327
- pull_number: draftPrNumber,
328
- request: { signal },
329
- });
330
- await ctx
331
- .octokit()
332
- .graphql(`mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, { id: pr.node_id, request: { signal } });
359
+ await ctx.github().convertPullRequestToDraft(draftPrNumber, signal);
333
360
  workerLog(`Converted PR #${draftPrNumber} to draft`);
334
361
  return { prNumber: draftPrNumber, draft: true };
335
362
  }
336
363
  case 'close-pr': {
337
364
  const closePrNumber = requireNumber(payload, 'pullRequestNumber');
338
- await ctx.octokit().pulls.update({
339
- owner: ctx.githubOwner,
340
- repo: ctx.githubRepo,
341
- pull_number: closePrNumber,
342
- state: 'closed',
343
- request: { signal },
344
- });
365
+ await ctx.github().updatePullRequest(closePrNumber, { state: 'closed' }, signal);
345
366
  return { closed: true };
346
367
  }
347
368
  case 'delete-remote-branch': {
348
369
  const branch = requireString(payload, 'branch');
349
- await ctx.octokit().git.deleteRef({
350
- owner: ctx.githubOwner,
351
- repo: ctx.githubRepo,
352
- ref: `heads/${branch}`,
353
- request: { signal },
354
- });
370
+ // Defense-in-depth behind the delete handler's own refusal.
371
+ if (sanitizeBranchName(branch) === ctx.sanitizedBaseBranch || isSettingsBranch(branch)) {
372
+ throw new PermanentTaskError(`Refusing to delete "${branch}" on GitHub: the base and settings branches are never deleted`);
373
+ }
374
+ // A branch that reused the name after this task was queued (a requeued task can run long
375
+ // after) owns the GitHub branch once it recorded a different PR or GitHub push. Without
376
+ // either, the ref is taken to be the deleted branch's. Corrupt metadata, which includes a
377
+ // file with no branch record, keeps the GitHub branch; other read errors retry.
378
+ const deletedPr = typeof payload.pullRequestNumber === 'number' ? payload.pullRequestNumber : undefined;
379
+ const deletedPushedAt = typeof payload.pushedToGitHubAt === 'string' ? payload.pushedToGitHubAt : undefined;
380
+ let live = null;
381
+ let unreadable = false;
382
+ try {
383
+ live = await BranchMetadataFileManager.loadOnly(ctx.branchWorkspacePath(branch));
384
+ }
385
+ catch (err) {
386
+ if (!(err instanceof BranchMetadataCorruptError))
387
+ throw err;
388
+ unreadable = true;
389
+ }
390
+ if (unreadable) {
391
+ workerLog(`Not deleting GitHub branch ${branch}: the branch now under that name is unreadable`);
392
+ return { deleted: false, skipped: 'metadata-unreadable' };
393
+ }
394
+ const livePr = live?.branch.pullRequestNumber;
395
+ if (livePr !== undefined && livePr !== deletedPr) {
396
+ workerLog(`Not deleting GitHub branch ${branch}: a newer branch's PR #${livePr} uses it`);
397
+ return { deleted: false, skipped: 'name-reused' };
398
+ }
399
+ const livePushedAt = live?.branch.pushedToGitHubAt;
400
+ if (livePushedAt !== undefined && livePushedAt !== deletedPushedAt) {
401
+ workerLog(`Not deleting GitHub branch ${branch}: a newer branch pushed under that name`);
402
+ return { deleted: false, skipped: 'name-reused' };
403
+ }
404
+ try {
405
+ await ctx.github().deleteBranch(branch, signal);
406
+ }
407
+ catch (err) {
408
+ // Already gone is the outcome this task wants; failing it would only park it in failed/.
409
+ if (!isRefAlreadyGoneError(err))
410
+ throw err;
411
+ workerLog(`GitHub branch ${branch} was already deleted`);
412
+ return { deleted: false, alreadyGone: true };
413
+ }
414
+ workerLog(`Deleted GitHub branch ${branch}`);
355
415
  return { deleted: true };
356
416
  }
357
417
  default:
358
418
  throw new PermanentTaskError(`Unknown task action: ${action}`);
359
419
  }
360
420
  }
421
+ /**
422
+ * The branch whose metadata records a task's outcome, or null when none does. A
423
+ * delete-remote-branch task names a branch already deleted here, so a workspace under that
424
+ * name is leftover or a newer branch's that reused the name.
425
+ */
426
+ function metadataBranchOf(task) {
427
+ if (task.action === 'delete-remote-branch')
428
+ return null;
429
+ return typeof task.payload.branch === 'string' ? task.payload.branch : null;
430
+ }
361
431
  /**
362
432
  * Update branch metadata after successful task completion.
363
433
  */
364
434
  export async function updateBranchMetadata(ctx, task, result) {
365
- const branch = typeof task.payload.branch === 'string' ? task.payload.branch : null;
435
+ const branch = metadataBranchOf(task);
366
436
  if (!branch)
367
437
  return;
368
438
  const branchPath = ctx.branchWorkspacePath(branch);
@@ -409,9 +479,14 @@ export async function updateBranchMetadata(ctx, task, result) {
409
479
  * Update branch metadata after permanent task failure. `error` is already
410
480
  * redacted by the caller (see [REDACT] in processTaskQueue) and is recorded as
411
481
  * syncFailureReason so the editor can show WHY, not just that it failed.
482
+ *
483
+ * `unlock` also returns the branch to 'editing', but only while it is still in
484
+ * the submit this task carries (its `submittedAt`), 'submitted' with no PR: a
485
+ * withdraw, a newer submit, or a review decision that landed meanwhile is left
486
+ * alone.
412
487
  */
413
- async function updateBranchMetadataOnFailure(ctx, task, error) {
414
- const branch = typeof task.payload.branch === 'string' ? task.payload.branch : null;
488
+ async function updateBranchMetadataOnFailure(ctx, task, error, options) {
489
+ const branch = metadataBranchOf(task);
415
490
  if (!branch)
416
491
  return;
417
492
  const branchPath = ctx.branchWorkspacePath(branch);
@@ -423,9 +498,21 @@ async function updateBranchMetadataOnFailure(ctx, task, error) {
423
498
  }
424
499
  try {
425
500
  const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
426
- await meta.save({
427
- branch: { name: branch, syncStatus: 'sync-failed', syncFailureReason: error },
428
- });
501
+ const failed = { name: branch, syncStatus: 'sync-failed', syncFailureReason: error };
502
+ const submittedAt = task.payload.submittedAt;
503
+ const unlocked = options.unlock &&
504
+ typeof submittedAt === 'string' &&
505
+ (await meta.saveIf({
506
+ branch: {
507
+ ...failed,
508
+ status: 'editing',
509
+ syncFailureReason: `${error} The branch is unlocked for editing; save a change, then submit again.`,
510
+ },
511
+ }, (existing) => existing?.branch.status === 'submitted' &&
512
+ existing.branch.pullRequestNumber === undefined &&
513
+ existing.branch.submittedAt === submittedAt)) !== null;
514
+ if (!unlocked)
515
+ await meta.save({ branch: failed });
429
516
  }
430
517
  catch (err) {
431
518
  workerLogError(`Failed to update failure metadata for ${branch}:`, err instanceof Error ? err.message : err);
@@ -448,37 +535,28 @@ function throwIfWorkflowRefusal(branch, message) {
448
535
  `retrying will not help until a developer with permission to change workflow files ` +
449
536
  `resolves it on GitHub.`);
450
537
  }
451
- export async function pushBranchToGitHub(ctx, branch) {
452
- const git = simpleGit({
453
- baseDir: ctx.remoteGitPath,
454
- // DEP-H1: kill the git process if it produces no output for taskTimeoutMs
455
- // (network stall, credential prompt) instead of letting it hang past the
456
- // task timeout.
457
- timeout: { block: ctx.taskTimeoutMs },
458
- });
459
- // Force stable (English) git output so isNonFastForwardRejection below can
460
- // match it -- git's rejection text is gettext-translated, so a non-English
461
- // host would silently turn that classifier into a no-op. gitNetworkChildEnv
462
- // (NOT gitChildEnv) because this call talks to GitHub: it keeps ambient
463
- // HTTPS_PROXY/GIT_SSL_*/GIT_SSH_COMMAND, which gitChildEnv's local-ops
464
- // allowlist deliberately drops.
465
- git.env(gitNetworkChildEnv());
466
- // Resolve the tokenized URL ONCE, here: all three pushes below use this
467
- // const and none calls ctx.buildGitHubUrl() again. A correctness
468
- // requirement, not tidiness, since resolution is async:
469
- //
470
- // - the retry push sits INSIDE the stale-lease catch, and a resolution that
471
- // threw there would replace the push error being classified, so neither
472
- // isStaleLeaseRejection nor isNonFastForwardRejection would run and a
473
- // genuinely diverged branch would be retried instead of raising
474
- // PermanentTaskError;
475
- // - readPublishedSha below captures remote.git's tip BEFORE the push, and
476
- // that value decides whether the [SYNC-H1] marker is cleared at the end.
477
- // An awaited resolution in between widens the window in which the tip can
478
- // move underneath that decision.
479
- //
480
- // It also means all three pushes provably carry the same credential.
481
- const githubUrl = await ctx.buildGitHubUrl();
538
+ export async function pushBranchToGitHub(ctx, branch, signal) {
539
+ // The credential reaches git only in the GitHub gateway's state directory, its mirror or a
540
+ // quarantine there (worker/github-mirror.ts); remote.git's config is Lambda-writable, and the
541
+ // bundle built here runs under the pins. The push sends exactly `outgoingSha`, the commit remote.git held when it
542
+ // was read, so what the marker logic at the end compares is what GitHub received.
543
+ await assertSharedRepoConfig(ctx.remoteGitPath, 'bare');
544
+ // Before `branch` reaches any git: `readPublishedSha` below would read a `<rev>:<path>` form as a
545
+ // tree lookup in remote.git. A refusal is permanent; a failure to run git is retried.
546
+ try {
547
+ await assertPlainBranchName(branch);
548
+ }
549
+ catch (err) {
550
+ if (err instanceof RefusedPushError)
551
+ throw new PermanentTaskError(err.message);
552
+ throw err;
553
+ }
554
+ // Defense-in-depth behind the gateway's policy, which protects a base detected rather than
555
+ // configured only when it is GitHub's default branch (BranchPolicy).
556
+ if (sanitizeBranchName(branch) === ctx.sanitizedBaseBranch) {
557
+ throw new PermanentTaskError(`Refusing to push "${branch}" to GitHub: it is the base branch, which the CanopyCMS worker ` +
558
+ `never pushes. Nothing in CanopyCMS queues such a push; find out what wrote this task.`);
559
+ }
482
560
  // [SYNC-H1] If the rebase loop rewrote this branch's already-published
483
561
  // history, GitHub still holds the commit it replaced, so an ordinary
484
562
  // push is non-fast-forward forever. Push under a lease keyed to exactly
@@ -487,61 +565,41 @@ export async function pushBranchToGitHub(ctx, branch) {
487
565
  const branchPath = ctx.branchWorkspacePath(branch);
488
566
  const metaFile = await BranchMetadataFileManager.loadOnly(branchPath).catch(() => null);
489
567
  const marker = metaFile?.branch.historyRewrittenFrom;
490
- // What this push will actually send: remote.git's tip for the branch.
491
- const outgoingSha = await readPublishedSha(ctx, branch);
492
- try {
493
- if (marker) {
494
- await git.raw([
495
- 'push',
496
- `--force-with-lease=${branch}:${marker}`,
497
- '--end-of-options',
498
- githubUrl,
499
- `${branch}:${branch}`,
500
- ]);
501
- }
502
- else {
503
- // Pass URL directly to avoid persisting the token in remote.git/config
504
- await git.push(githubUrl, branch);
568
+ const readSha = async () => {
569
+ const sha = await readPublishedSha(ctx, branch);
570
+ if (sha === null) {
571
+ throw new Error(`Branch "${branch}" is not in remote.git, so there is nothing to push`);
505
572
  }
573
+ return sha;
574
+ };
575
+ // The commit GitHub was sent: the transfer reads it again if the branch moves under it.
576
+ let outgoingSha;
577
+ let outcome;
578
+ try {
579
+ ;
580
+ ({ sha: outgoingSha, outcome } = await pushToGitHub(ctx, {
581
+ branch,
582
+ lease: marker,
583
+ prerequisiteBranches: [branch, ctx.baseBranch],
584
+ }, readSha, signal));
506
585
  }
507
586
  catch (err) {
508
- const message = getErrorMessage(err);
509
- throwIfWorkflowRefusal(branch, message);
510
- // A refused lease means GitHub is not at the commit we rewrote, so the
511
- // marker is stale -- routine, not exceptional: tasks are re-run after a
512
- // crash (recoverOrphanedTasks) and the marker survives any failure to
513
- // clear it. The two benign shapes that reach here (GitHub already holds
514
- // the rewritten history, or the branch moved past it) are ordinary
515
- // fast-forwards a lease has no business blocking.
516
- //
517
- // So retry PLAIN and let git adjudicate: a non-forced push succeeds if and
518
- // only if it fast-forwards, so it can never destroy anything, with no
519
- // ancestry check to get wrong and no extra round trip to read GitHub's
520
- // tip. Only if THAT is also rejected has the branch genuinely diverged.
521
- //
522
- // (git evaluates the lease only when it actually has an update to apply,
523
- // so an up-to-date ref with a stale lease prints "Everything up-to-date",
524
- // exits 0, and is absorbed above without reaching here.)
525
- if (marker && isStaleLeaseRejection(message)) {
526
- try {
527
- await git.push(githubUrl, branch);
528
- }
529
- catch (retryErr) {
530
- const retryMessage = getErrorMessage(retryErr);
531
- throwIfWorkflowRefusal(branch, retryMessage);
532
- if (isNonFastForwardRejection(retryMessage)) {
533
- throw new PermanentTaskError(`Push rejected for branch "${branch}": GitHub's tip is neither the commit this ` +
534
- `deployment last published nor an ancestor of what it is pushing, so the branch ` +
535
- `has genuinely diverged and nothing was overwritten. Something else moved it on ` +
536
- `GitHub -- a direct push, or another CanopyCMS deployment sharing this repository.`);
537
- }
538
- throw retryErr;
587
+ // A retry can never make the branch not protected.
588
+ if (err instanceof RefusedPushError)
589
+ throw new PermanentTaskError(err.message);
590
+ if (!(err instanceof GitHubPushError))
591
+ throw err;
592
+ throwIfWorkflowRefusal(branch, err.message);
593
+ // A refused lease was already retried plain, and that retry rejected too: only a
594
+ // non-fast-forward then means the branch has genuinely diverged.
595
+ if (err.kind === 'rejected-after-stale-lease') {
596
+ if (isNonFastForwardRejection(err.message)) {
597
+ throw new PermanentTaskError(`Push rejected for branch "${branch}": GitHub's tip is neither the commit this ` +
598
+ `deployment last published nor an ancestor of what it is pushing, so the branch ` +
599
+ `has genuinely diverged and nothing was overwritten. Something else moved it on ` +
600
+ `GitHub -- a direct push, or another CanopyCMS deployment sharing this repository.`);
539
601
  }
540
- // The lease was refused, so GitHub is provably not at the marker: it
541
- // has moved past the rewritten commit and the marker is spent.
542
- await clearHistoryRewrittenMarker(ctx, branchPath, branch);
543
- workerLog(`Pushed ${branch} to GitHub (GitHub had already moved past the rewritten commit)`);
544
- return;
602
+ throw new Error(err.message);
545
603
  }
546
604
  // An ordinary non-fast-forward rejection: GitHub has commits this
547
605
  // deployment never published, so retrying the identical push can never
@@ -549,19 +607,62 @@ export async function pushBranchToGitHub(ctx, branch) {
549
607
  // it fails fast instead of burning the retry budget. Deliberately does NOT
550
608
  // advise renaming the branch: one reaching this point usually has an open
551
609
  // PR, which renaming would orphan.
552
- if (isNonFastForwardRejection(message)) {
610
+ if (isNonFastForwardRejection(err.message)) {
553
611
  throw new PermanentTaskError(`Push rejected for branch "${branch}": GitHub's tip is not what this deployment last ` +
554
612
  `published, so the branch has diverged and needs reconciling. Something else moved it ` +
555
613
  `on GitHub -- a direct push, or another CanopyCMS deployment sharing this repository.`);
556
614
  }
557
- throw err;
615
+ throw new Error(err.message);
616
+ }
617
+ if (outcome.pastStaleLease) {
618
+ // The lease was refused, so GitHub is provably not at the marker: it
619
+ // has moved past the rewritten commit and the marker is spent.
620
+ await clearHistoryRewrittenMarker(ctx, branchPath, branch);
621
+ await recordPushedToGitHub(ctx, branchPath, branch);
622
+ logPushOutcome(ctx, branch, outcome, 'GitHub had already moved past the rewritten commit');
623
+ return;
558
624
  }
559
625
  // Clear the marker only once GitHub is confirmed to hold something other
560
626
  // than the commit we rewrote. A push that sent nothing new (remote.git
561
627
  // still at the marker because its own publish has not landed yet) must
562
628
  // leave the marker set -- it is the sole trigger for the self-heal pass.
563
- if (marker && outgoingSha && outgoingSha !== marker) {
629
+ if (marker && outgoingSha !== marker) {
564
630
  await clearHistoryRewrittenMarker(ctx, branchPath, branch);
565
631
  }
566
- workerLog(`Pushed ${branch} to GitHub`);
632
+ await recordPushedToGitHub(ctx, branchPath, branch);
633
+ logPushOutcome(ctx, branch, outcome);
634
+ }
635
+ /** An unmoved ref is routine (a task re-run after a crash), so it is logged at debug level. */
636
+ function logPushOutcome(ctx, branch, outcome, note) {
637
+ const suffix = note === undefined ? '' : `; ${note}`;
638
+ if (outcome.moved) {
639
+ workerLog(`Pushed ${branch} to GitHub (${describeMove(outcome)}${suffix})`);
640
+ }
641
+ else {
642
+ ctx.log.debug(`${branch} already up to date on GitHub${suffix}`);
643
+ }
644
+ }
645
+ /**
646
+ * Stamp `pushedToGitHubAt`, branch delete's proof that the GitHub branch is this one. Recorded
647
+ * when the push lands, whatever the PR call after it does. Best-effort: a branch deleted
648
+ * meanwhile has no metadata to stamp, and a failed stamp only leaves a later delete to skip
649
+ * GitHub.
650
+ */
651
+ async function recordPushedToGitHub(ctx, branchPath, branch) {
652
+ if (isSettingsBranch(branch))
653
+ return;
654
+ try {
655
+ await fs.stat(branchPath);
656
+ }
657
+ catch {
658
+ return;
659
+ }
660
+ try {
661
+ await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).save({
662
+ branch: { name: branch, pushedToGitHubAt: new Date().toISOString() },
663
+ });
664
+ }
665
+ catch (err) {
666
+ workerLogError(`Failed to record the GitHub push for ${branch}:`, getErrorMessage(err));
667
+ }
567
668
  }