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.
- package/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- package/dist/editor/components/EditorSidebar.js +0 -22
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { simpleGit } from 'simple-git';
|
|
4
|
+
import { gitChildEnv } from '../git-manager.js';
|
|
5
|
+
import { getErrorMessage } from '../utils/error.js';
|
|
6
|
+
/**
|
|
7
|
+
* How the worker runs git in a repository the CMS Lambda can also write: `remote.git` and every
|
|
8
|
+
* branch clone on the shared filesystem. Their `.git/config` and hooks are Lambda-writable, and
|
|
9
|
+
* the worker runs as the user that holds the GitHub credential, so git there must neither run
|
|
10
|
+
* anything those files name nor reach anywhere they point. The credential itself never enters
|
|
11
|
+
* these repositories; worker/github-mirror.ts holds it.
|
|
12
|
+
*
|
|
13
|
+
* Two layers. The `-c` pins below override repository config for every key they name, which
|
|
14
|
+
* holds against a concurrent writer. Keys whose names the attacker chooses cannot be pinned
|
|
15
|
+
* (`filter.<driver>.*`, `merge.<driver>.driver`, `url.<base>.insteadOf`, `http.<url>.*`), so
|
|
16
|
+
* {@link assertSharedRepoConfig} refuses a repository whose own config holds anything outside
|
|
17
|
+
* {@link ALLOWED_REPO_CONFIG_KEYS}, or a submodule with a repository in it. That check reads
|
|
18
|
+
* before git does, so a writer racing it still runs code in a working-tree operation (rebase,
|
|
19
|
+
* merge, checkout) through a driver it plants in between, as does one that edits a rebase the
|
|
20
|
+
* worker has stopped at a conflict (an `exec` line in `git-rebase-todo`, the `strategy` file). Only
|
|
21
|
+
* moving that git into a process without the credential closes those:
|
|
22
|
+
* .claude/future-tasks/resolved/worker-shared-repo-git-process-split.md.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Every hook event in githooks(5) at git 2.55 except git-p4's, which only git-p4 fires and the worker
|
|
26
|
+
* never runs. `hook.<event>.enabled=false` is what stops a
|
|
27
|
+
* config-defined hook (`hook.<name>.command` + `.event`), which `core.hooksPath` does not; a git
|
|
28
|
+
* without config hooks ignores the key.
|
|
29
|
+
*/
|
|
30
|
+
const HOOK_EVENTS = [
|
|
31
|
+
'applypatch-msg',
|
|
32
|
+
'pre-applypatch',
|
|
33
|
+
'post-applypatch',
|
|
34
|
+
'pre-commit',
|
|
35
|
+
'pre-merge-commit',
|
|
36
|
+
'prepare-commit-msg',
|
|
37
|
+
'commit-msg',
|
|
38
|
+
'post-commit',
|
|
39
|
+
'pre-rebase',
|
|
40
|
+
'post-checkout',
|
|
41
|
+
'post-merge',
|
|
42
|
+
'pre-push',
|
|
43
|
+
'pre-receive',
|
|
44
|
+
'update',
|
|
45
|
+
'proc-receive',
|
|
46
|
+
'post-receive',
|
|
47
|
+
'post-update',
|
|
48
|
+
'reference-transaction',
|
|
49
|
+
'push-to-checkout',
|
|
50
|
+
'pre-auto-gc',
|
|
51
|
+
'post-rewrite',
|
|
52
|
+
'sendemail-validate',
|
|
53
|
+
'fsmonitor-watchman',
|
|
54
|
+
'post-index-change',
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Transports git can reach without a helper on PATH. A `protocol.<name>.allow` in repo config
|
|
58
|
+
* outranks the generic `protocol.allow`, so each is pinned by name.
|
|
59
|
+
*/
|
|
60
|
+
const DENIED_TRANSPORTS = ['ext', 'fd', 'ssh', 'git', 'http', 'https', 'ftp', 'ftps'];
|
|
61
|
+
/**
|
|
62
|
+
* `-c` settings for every worker git process in a shared repository, and for each process those
|
|
63
|
+
* spawn: git passes them on through `GIT_CONFIG_PARAMETERS`, except to the `upload-pack` or
|
|
64
|
+
* `receive-pack` of a local fetch or push, which {@link pinnedUploadPack} and
|
|
65
|
+
* {@link pinnedReceivePack} pin on their own command line. Each entry is shell-safe, because
|
|
66
|
+
* those two commands run through `sh -c`.
|
|
67
|
+
*/
|
|
68
|
+
const SHARED_REPO_PINS = [
|
|
69
|
+
'core.hooksPath=/dev/null',
|
|
70
|
+
...HOOK_EVENTS.map((event) => `hook.${event}.enabled=false`),
|
|
71
|
+
'core.fsmonitor=false',
|
|
72
|
+
// Advertising an alternate's refs runs this through the shell, or spawns git in the alternate
|
|
73
|
+
// directory; the alternates file is Lambda-writable too.
|
|
74
|
+
'core.alternateRefsCommand=true',
|
|
75
|
+
'core.editor=true',
|
|
76
|
+
'sequence.editor=true',
|
|
77
|
+
'credential.helper=',
|
|
78
|
+
'core.askPass=',
|
|
79
|
+
'protocol.allow=never',
|
|
80
|
+
'protocol.file.allow=always',
|
|
81
|
+
...DENIED_TRANSPORTS.map((transport) => `protocol.${transport}.allow=never`),
|
|
82
|
+
'commit.gpgSign=false',
|
|
83
|
+
'tag.gpgSign=false',
|
|
84
|
+
'push.gpgSign=false',
|
|
85
|
+
// A merge that verifies signatures runs the signing program on whatever signature a commit in
|
|
86
|
+
// remote.git carries; the programs are pinned too, so nothing else that verifies runs one.
|
|
87
|
+
'merge.verifySignatures=false',
|
|
88
|
+
'pull.verifySignatures=false',
|
|
89
|
+
'log.showSignature=false',
|
|
90
|
+
'gpg.program=false',
|
|
91
|
+
'gpg.ssh.program=false',
|
|
92
|
+
'gpg.x509.program=false',
|
|
93
|
+
// Negotiating a push runs the destination's upload-pack, unpinned.
|
|
94
|
+
'push.negotiate=false',
|
|
95
|
+
'submodule.recurse=false',
|
|
96
|
+
'diff.ignoreSubmodules=all',
|
|
97
|
+
// The commit `rebase --continue` makes would otherwise summarise status, descending into a
|
|
98
|
+
// submodule under that submodule's own config.
|
|
99
|
+
'commit.status=false',
|
|
100
|
+
'fetch.recurseSubmodules=false',
|
|
101
|
+
'push.recurseSubmodules=no',
|
|
102
|
+
'gc.auto=0',
|
|
103
|
+
'maintenance.auto=false',
|
|
104
|
+
];
|
|
105
|
+
/** What a bare shared repository adds: never a working tree, so never a checkout. */
|
|
106
|
+
const BARE_PINS = ['core.bare=true'];
|
|
107
|
+
/**
|
|
108
|
+
* What the worker's private GitHub mirror runs with (worker/github-mirror.ts). Nothing the Lambda
|
|
109
|
+
* writes reaches that repository's config, so these are depth, not the boundary: the same pins,
|
|
110
|
+
* HTTPS allowed for GitHub, and no credential helper ever handed the token. Pushed objects are
|
|
111
|
+
* checked where a quarantine takes them in (worker/github-mirror.ts).
|
|
112
|
+
*/
|
|
113
|
+
const MIRROR_PINS = [
|
|
114
|
+
...SHARED_REPO_PINS.filter((pin) => pin !== 'protocol.https.allow=never'),
|
|
115
|
+
'protocol.https.allow=always',
|
|
116
|
+
// A host's own core.sharedRepository would make `init` create it group-writable.
|
|
117
|
+
'core.sharedRepository=umask',
|
|
118
|
+
...BARE_PINS,
|
|
119
|
+
];
|
|
120
|
+
/** simple-git refuses each of these pinned keys, and the pack-command options, unless opted in. */
|
|
121
|
+
const PIN_OPT_INS = {
|
|
122
|
+
allowUnsafeHooksPath: true,
|
|
123
|
+
allowUnsafeFsMonitor: true,
|
|
124
|
+
allowUnsafeEditor: true,
|
|
125
|
+
allowUnsafeCredentialHelper: true,
|
|
126
|
+
allowUnsafeAskPass: true,
|
|
127
|
+
allowUnsafeProtocolOverride: true,
|
|
128
|
+
allowUnsafeGpgProgram: true,
|
|
129
|
+
allowUnsafePack: true,
|
|
130
|
+
};
|
|
131
|
+
function pinsFor(kind) {
|
|
132
|
+
return kind === 'bare' ? [...SHARED_REPO_PINS, ...BARE_PINS] : [...SHARED_REPO_PINS];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The pins alone, without the environment {@link sharedRepoGit} adds (an explicit `GIT_DIR`, no
|
|
136
|
+
* lazy fetches), which every worker git in a shared repository needs too.
|
|
137
|
+
* @internal Exported for tests.
|
|
138
|
+
*/
|
|
139
|
+
export function sharedRepoGitOptions(kind) {
|
|
140
|
+
return { config: pinsFor(kind), unsafe: { ...PIN_OPT_INS } };
|
|
141
|
+
}
|
|
142
|
+
/** The git directory of a shared repository: itself when bare, its `.git` otherwise. */
|
|
143
|
+
function gitDirOf(repoPath, kind) {
|
|
144
|
+
return kind === 'bare' ? repoPath : path.join(repoPath, '.git');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A worker git instance in the shared repository at `repoPath` (the bare repository itself, or a
|
|
148
|
+
* clone's working tree), with the pins applied. The repository is named outright, as
|
|
149
|
+
* {@link assertSharedRepoConfig} names it, so both read the same config: discovery would take a
|
|
150
|
+
* planted `remote.git/.git`, or a `.git` above a clone whose own was removed. A clone's working
|
|
151
|
+
* tree is named too, which also overrides `core.worktree`.
|
|
152
|
+
*
|
|
153
|
+
* `options` are the caller's own (`timeout`, `abort`), applied under the pins. The child env is
|
|
154
|
+
* `gitChildEnv`'s: every shared-repository operation is local.
|
|
155
|
+
*/
|
|
156
|
+
export function sharedRepoGit(repoPath, kind, options = {}) {
|
|
157
|
+
const absolute = path.resolve(repoPath);
|
|
158
|
+
return simpleGit({ ...options, baseDir: absolute, ...sharedRepoGitOptions(kind) }).env(sharedRepoEnv(absolute, kind));
|
|
159
|
+
}
|
|
160
|
+
/** simple-git options for the worker's private GitHub mirror. */
|
|
161
|
+
export function mirrorGitOptions() {
|
|
162
|
+
return { config: [...MIRROR_PINS], unsafe: { ...PIN_OPT_INS } };
|
|
163
|
+
}
|
|
164
|
+
/** {@link mirrorGitOptions}'s pins, for a git the mirror runs without simple-git (worker/git-process.ts). */
|
|
165
|
+
export function mirrorPins() {
|
|
166
|
+
return [...MIRROR_PINS];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The pins of a quarantine repository, where the GitHub gateway takes in a pushed bundle
|
|
170
|
+
* (worker/github-mirror.ts): the mirror's, without `core.alternateRefsCommand=true`. That pin
|
|
171
|
+
* guards an alternates file the Lambda can write; a quarantine's names the mirror and is written
|
|
172
|
+
* by the gateway. With it, the fetch's connectivity check could not stop at the mirror's heads and
|
|
173
|
+
* would walk the mirror's whole history for every push.
|
|
174
|
+
*/
|
|
175
|
+
export function quarantinePins() {
|
|
176
|
+
return MIRROR_PINS.filter((pin) => pin !== 'core.alternateRefsCommand=true');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* {@link sharedRepoGit}'s pins, environment and working directory, for a git run through
|
|
180
|
+
* worker/git-process.ts: one whose stdin or stdout is a stream.
|
|
181
|
+
*/
|
|
182
|
+
export function sharedRepoProcessOptions(repoPath, kind) {
|
|
183
|
+
const absolute = path.resolve(repoPath);
|
|
184
|
+
return {
|
|
185
|
+
cwd: absolute,
|
|
186
|
+
env: sharedRepoEnv(absolute, kind),
|
|
187
|
+
config: pinsFor(kind),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function sharedRepoEnv(absolute, kind) {
|
|
191
|
+
return gitChildEnv({
|
|
192
|
+
GIT_DIR: gitDirOf(absolute, kind),
|
|
193
|
+
...(kind === 'worktree' ? { GIT_WORK_TREE: absolute } : {}),
|
|
194
|
+
// A missing object would otherwise be fetched from a promisor remote the config names, by
|
|
195
|
+
// that remote's own upload-pack command (git 2.45+; older git ignores it).
|
|
196
|
+
GIT_NO_LAZY_FETCH: '1',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function packCommand(program, extra, flags = []) {
|
|
200
|
+
return [
|
|
201
|
+
'git',
|
|
202
|
+
...[...pinsFor('bare'), ...extra].flatMap((pin) => ['-c', pin]),
|
|
203
|
+
program,
|
|
204
|
+
...flags,
|
|
205
|
+
].join(' ');
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `--upload-pack` for a fetch FROM `remote.git` into a clone.
|
|
209
|
+
* @internal Exported for tests.
|
|
210
|
+
*/
|
|
211
|
+
export function pinnedUploadPack() {
|
|
212
|
+
// --strict: never `<path>/.git` in place of the repository named.
|
|
213
|
+
return packCommand('upload-pack', [], ['--strict']);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* `--receive-pack` for a push INTO `remote.git`. Without it the push runs remote.git's own hooks
|
|
217
|
+
* as the worker. The `receive.deny*` pins keep a planted setting from blocking the forced update
|
|
218
|
+
* that publishes a rewritten history (worker/history-rewrite.ts).
|
|
219
|
+
*/
|
|
220
|
+
export function pinnedReceivePack() {
|
|
221
|
+
return packCommand('receive-pack', [
|
|
222
|
+
'receive.autogc=false',
|
|
223
|
+
'receive.denyDeletes=false',
|
|
224
|
+
'receive.denyNonFastForwards=false',
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Fetch `branch` from `remote.git` into the clone `git` runs in, through
|
|
229
|
+
* {@link pinnedUploadPack}. The result is in FETCH_HEAD only.
|
|
230
|
+
*/
|
|
231
|
+
export async function fetchFromRemoteGit(git, remoteGitPath, branch) {
|
|
232
|
+
await git.raw([
|
|
233
|
+
'fetch',
|
|
234
|
+
`--upload-pack=${pinnedUploadPack()}`,
|
|
235
|
+
'--end-of-options',
|
|
236
|
+
remoteGitPath,
|
|
237
|
+
branch,
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Repository config keys CanopyCMS or git itself writes into a shared repository, in the form
|
|
242
|
+
* `git config --list` prints them (section and key lowercased, subsection verbatim). Covers every
|
|
243
|
+
* writer in this repository's history: clone (`core.*`, `remote.*`, `branch.*`, plus the `-c`
|
|
244
|
+
* settings it persists: `managedWorkspaceConfig`'s and gc's), `ensureAuthor`, `ensureRemote`,
|
|
245
|
+
* `push --set-upstream`, `sparse-checkout set` (`extensions.worktreeConfig` and the cone keys in
|
|
246
|
+
* `config.worktree`), `REMOTE_GIT_CONFIG`, and the `remote.origin.url` a pre-scrub bare clone of
|
|
247
|
+
* GitHub left in `remote.git`. A named remote's `url` is inert here because the worker addresses
|
|
248
|
+
* remotes by absolute path, which git never reads as a remote's name, and the transport pins hold
|
|
249
|
+
* whatever it says.
|
|
250
|
+
*/
|
|
251
|
+
const ALLOWED_REPO_CONFIG_KEYS = [
|
|
252
|
+
/^core\.(repositoryformatversion|filemode|bare|logallrefupdates|ignorecase|precomposeunicode|symlinks|sparsecheckout|sparsecheckoutcone)$/,
|
|
253
|
+
/^extensions\.(worktreeconfig|objectformat|refstorage)$/,
|
|
254
|
+
/^index\.sparse$/,
|
|
255
|
+
/^remote\..+\.(url|fetch)$/,
|
|
256
|
+
/^branch\..+\.(remote|merge)$/,
|
|
257
|
+
/^user\.(name|email)$/,
|
|
258
|
+
/^canopycms\.managed$/,
|
|
259
|
+
/^gc\.auto$/,
|
|
260
|
+
/^maintenance\.auto$/,
|
|
261
|
+
/^receive\.autogc$/,
|
|
262
|
+
/^transfer\.unpacklimit$/,
|
|
263
|
+
];
|
|
264
|
+
/**
|
|
265
|
+
* How a refusal's fix is to be run. Run as root, or as an unsandboxed worker user, a command on EFS
|
|
266
|
+
* can be made to run what the CMS Lambda wrote there (a repository's config, a symbolic link), and
|
|
267
|
+
* root writes into EFS follow its links; `canopy-inspect` (the AWS worker instance's
|
|
268
|
+
* /opt/canopy-worker/canopy-inspect) runs each one as the worker does, inside its sandbox.
|
|
269
|
+
*/
|
|
270
|
+
const RUN_FIX_AS_THE_WORKER = "as the worker's user and never as root: on the AWS worker instance, run each command below " +
|
|
271
|
+
"on its own as /opt/canopy-worker/canopy-inspect <command>; '; then ' separates them";
|
|
272
|
+
/** Every refusal to run git in a shared repository, so a caller can tell one from git's own errors. */
|
|
273
|
+
export class SharedRepoRefusalError extends Error {
|
|
274
|
+
constructor(message) {
|
|
275
|
+
super(message);
|
|
276
|
+
this.name = 'SharedRepoRefusalError';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A shared repository whose own config holds a key CanopyCMS never writes.
|
|
281
|
+
* @internal Exported for tests.
|
|
282
|
+
*/
|
|
283
|
+
export class UntrustedRepoConfigError extends SharedRepoRefusalError {
|
|
284
|
+
constructor(repoPath, keys) {
|
|
285
|
+
// Keys and files only: a value can be a credential.
|
|
286
|
+
super(`Refusing to run git in ${repoPath}: its git config holds ` +
|
|
287
|
+
`${keys.length === 1 ? 'a setting' : `${keys.length} settings`} CanopyCMS never writes, ` +
|
|
288
|
+
`which can make the worker run a command or send its GitHub credential elsewhere ` +
|
|
289
|
+
`(${keys.map((k) => `${k.key} in ${k.file}`).join('; ')}). Find out how ` +
|
|
290
|
+
`${keys.length === 1 ? 'it' : 'they'} got there, then remove ` +
|
|
291
|
+
`${keys.length === 1 ? 'it' : 'them'}, ${RUN_FIX_AS_THE_WORKER}: ` +
|
|
292
|
+
keys
|
|
293
|
+
.map((k) => `git config --file ${shellQuote(k.file)} --unset-all ${shellQuote(k.key)}`)
|
|
294
|
+
.join('; then '));
|
|
295
|
+
this.repoPath = repoPath;
|
|
296
|
+
this.keys = keys;
|
|
297
|
+
this.name = 'UntrustedRepoConfigError';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** A shared repository whose config git could not read, or did not finish reading. */
|
|
301
|
+
function unreadableConfig(repoPath, configFile, err) {
|
|
302
|
+
const cause = getErrorMessage(err).trim();
|
|
303
|
+
const timedOut = /block timeout/i.test(cause);
|
|
304
|
+
return new SharedRepoRefusalError(`Refusing to run git in ${repoPath}: ` +
|
|
305
|
+
(timedOut
|
|
306
|
+
? `reading its git config did not finish, which an include.path naming a pipe or device ` +
|
|
307
|
+
`causes. Check ${configFile} and every file it includes.`
|
|
308
|
+
: `git could not read its config (${cause}). Check ${configFile} and every file it ` +
|
|
309
|
+
`includes.`));
|
|
310
|
+
}
|
|
311
|
+
/** A clone whose index git could not list. */
|
|
312
|
+
function unreadableIndex(clone, err) {
|
|
313
|
+
return new SharedRepoRefusalError(`Refusing to run git in ${clone}: git could not list its index (${getErrorMessage(err).trim()}).`);
|
|
314
|
+
}
|
|
315
|
+
async function pathExists(file) {
|
|
316
|
+
return fs.lstat(file).then(() => true, () => false);
|
|
317
|
+
}
|
|
318
|
+
function shellQuote(value) {
|
|
319
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* An `include.path` naming a pipe or `/dev/stdin` blocks every read of the config for ever; the
|
|
323
|
+
* timeout turns the check's read into a refusal. sparse-cone.ts's earlier read has its own.
|
|
324
|
+
*/
|
|
325
|
+
const CHECK_TIMEOUT_MS = 30_000;
|
|
326
|
+
/** Scopes the repository itself supplies; `system`, `global` and `command` are the worker's. */
|
|
327
|
+
const REPO_SCOPES = new Set(['local', 'worktree']);
|
|
328
|
+
/**
|
|
329
|
+
* Refuse a shared repository whose own config (with its includes, and `config.worktree`) holds a
|
|
330
|
+
* key outside {@link ALLOWED_REPO_CONFIG_KEYS}, or a submodule with a repository in it. Throws
|
|
331
|
+
* {@link UntrustedRepoConfigError} for a key. A config git cannot read, or does not finish reading,
|
|
332
|
+
* is also refused, with git's own message for a bad line, which names the line but not its content.
|
|
333
|
+
*
|
|
334
|
+
* `repoPath` is the bare repository itself for `bare`, the working tree for `worktree`.
|
|
335
|
+
*/
|
|
336
|
+
export async function assertSharedRepoConfig(repoPath, kind, options = {}) {
|
|
337
|
+
const timeout = {
|
|
338
|
+
timeout: { block: options.timeoutMs ?? CHECK_TIMEOUT_MS },
|
|
339
|
+
};
|
|
340
|
+
const absolute = path.resolve(repoPath);
|
|
341
|
+
const gitDir = gitDirOf(absolute, kind);
|
|
342
|
+
let listing;
|
|
343
|
+
try {
|
|
344
|
+
// `git config --list` runs outside a repository too, reading no repository config at all. A
|
|
345
|
+
// symlink, or a `commondir` file, would aim the operation at another repository, such as the
|
|
346
|
+
// worker's own mirror.
|
|
347
|
+
for (const entry of kind === 'bare' ? [gitDir] : [absolute, gitDir]) {
|
|
348
|
+
const stat = await fs.lstat(entry);
|
|
349
|
+
if (!stat.isDirectory())
|
|
350
|
+
throw new Error(`${entry} is not a directory`);
|
|
351
|
+
}
|
|
352
|
+
if (await pathExists(path.join(gitDir, 'commondir'))) {
|
|
353
|
+
throw new Error(`${path.join(gitDir, 'commondir')} exists, and git would use another repository`);
|
|
354
|
+
}
|
|
355
|
+
// receive-pack, given a bare repository's path, uses `<path>/.git` when there is one.
|
|
356
|
+
if (kind === 'bare' && (await pathExists(path.join(gitDir, '.git')))) {
|
|
357
|
+
throw new Error(`${path.join(gitDir, '.git')} exists, and git would use it instead`);
|
|
358
|
+
}
|
|
359
|
+
listing = await sharedRepoGit(absolute, kind, timeout).raw([
|
|
360
|
+
'config',
|
|
361
|
+
'--list',
|
|
362
|
+
'--show-scope',
|
|
363
|
+
'--show-origin',
|
|
364
|
+
'-z',
|
|
365
|
+
]);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
throw unreadableConfig(absolute, path.join(gitDir, 'config'), err);
|
|
369
|
+
}
|
|
370
|
+
const unexpected = [];
|
|
371
|
+
// -z prints each entry as `scope NUL origin NUL key LF value NUL`.
|
|
372
|
+
const fields = listing.split('\0');
|
|
373
|
+
for (let i = 0; i + 2 < fields.length; i += 3) {
|
|
374
|
+
const [scope, origin, entry] = [fields[i], fields[i + 1], fields[i + 2]];
|
|
375
|
+
if (!REPO_SCOPES.has(scope))
|
|
376
|
+
continue;
|
|
377
|
+
const newline = entry.indexOf('\n');
|
|
378
|
+
const key = newline === -1 ? entry : entry.slice(0, newline);
|
|
379
|
+
if (ALLOWED_REPO_CONFIG_KEYS.some((allowed) => allowed.test(key)))
|
|
380
|
+
continue;
|
|
381
|
+
const file = origin.startsWith('file:') ? origin.slice('file:'.length) : origin;
|
|
382
|
+
unexpected.push({ key, file });
|
|
383
|
+
}
|
|
384
|
+
if (unexpected.length > 0)
|
|
385
|
+
throw new UntrustedRepoConfigError(absolute, unexpected);
|
|
386
|
+
if (kind === 'worktree')
|
|
387
|
+
await assertNoSubmodules(absolute, timeout);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Refuse a clone whose index holds a submodule with a repository in it. Git runs inside one, under
|
|
391
|
+
* that repository's own config, which no allowlist here reads. `commit.status=false` and
|
|
392
|
+
* {@link SHARED_REPO_STATUS_ARGS} keep status, and the commit a continued rebase makes, out of one
|
|
393
|
+
* planted later; this refuses the plant itself.
|
|
394
|
+
* An unpopulated submodule, which a non-recursive clone of a repository that has one leaves, is
|
|
395
|
+
* harmless and allowed.
|
|
396
|
+
*/
|
|
397
|
+
async function assertNoSubmodules(clone, timeout) {
|
|
398
|
+
let listing;
|
|
399
|
+
try {
|
|
400
|
+
// --sparse: a sparse index stays collapsed rather than being expanded for every clone.
|
|
401
|
+
listing = await sharedRepoGit(clone, 'worktree', timeout).raw([
|
|
402
|
+
'ls-files',
|
|
403
|
+
'--stage',
|
|
404
|
+
'--sparse',
|
|
405
|
+
'-z',
|
|
406
|
+
]);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
throw unreadableIndex(clone, err);
|
|
410
|
+
}
|
|
411
|
+
const gitlinks = listing
|
|
412
|
+
.split('\0')
|
|
413
|
+
.filter((entry) => entry.startsWith('160000 '))
|
|
414
|
+
.map((entry) => entry.slice(entry.indexOf('\t') + 1));
|
|
415
|
+
await refusePopulated(clone, gitlinks);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Refuse a rebase or fast-forward from `from` to `to` in `clone` that would bring in a submodule
|
|
419
|
+
* at a path where a repository already sits, as one planted ahead of it would.
|
|
420
|
+
*/
|
|
421
|
+
export async function assertNoIncomingSubmodules(git, clone, from, to) {
|
|
422
|
+
// Raw diff-tree records: `:oldmode newmode oldsha newsha status` NUL path NUL.
|
|
423
|
+
const fields = (await git.raw(['diff-tree', '-r', '-z', '--no-renames', '--end-of-options', from, to])).split('\0');
|
|
424
|
+
const incoming = [];
|
|
425
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
426
|
+
const newMode = fields[i].split(' ')[1];
|
|
427
|
+
if (newMode === '160000')
|
|
428
|
+
incoming.push(fields[i + 1]);
|
|
429
|
+
}
|
|
430
|
+
await refusePopulated(clone, incoming);
|
|
431
|
+
}
|
|
432
|
+
async function refusePopulated(clone, gitlinks) {
|
|
433
|
+
const populated = [];
|
|
434
|
+
// A conflicted gitlink is listed once per stage.
|
|
435
|
+
for (const gitlink of new Set(gitlinks)) {
|
|
436
|
+
if (await pathExists(path.join(clone, gitlink, '.git')))
|
|
437
|
+
populated.push(gitlink);
|
|
438
|
+
}
|
|
439
|
+
if (populated.length === 0)
|
|
440
|
+
return;
|
|
441
|
+
const removals = [];
|
|
442
|
+
for (const gitlink of populated) {
|
|
443
|
+
const planted = path.join(clone, gitlink, '.git');
|
|
444
|
+
// A symbolic link on the way would make `rm -rf` remove whatever it names instead.
|
|
445
|
+
const real = await fs.realpath(planted).catch(() => planted);
|
|
446
|
+
removals.push(real === planted
|
|
447
|
+
? `rm -rf ${shellQuote(planted)}`
|
|
448
|
+
: `remove the symbolic link that makes ${shellQuote(planted)} resolve to ${shellQuote(real)}`);
|
|
449
|
+
}
|
|
450
|
+
throw new SharedRepoRefusalError(`Refusing to run git in ${clone}: it has a submodule with a repository in it at ` +
|
|
451
|
+
`${populated.map((p) => JSON.stringify(p)).join(', ')}, and git runs inside one under its ` +
|
|
452
|
+
`own config. CanopyCMS never populates a submodule. Find out how it got there, then, ` +
|
|
453
|
+
`${RUN_FIX_AS_THE_WORKER}: ${removals.join('; then ')}`);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Arguments for every worker `git status` in a clone. On the command line, unlike in config, the
|
|
457
|
+
* setting outranks a `.gitmodules` entry's own `ignore`.
|
|
458
|
+
*/
|
|
459
|
+
export const SHARED_REPO_STATUS_ARGS = ['--ignore-submodules=all'];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SparseConeReport {
|
|
2
|
+
reapplied: string[];
|
|
3
|
+
failed: {
|
|
4
|
+
branch: string;
|
|
5
|
+
error: string;
|
|
6
|
+
}[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Move each provisioned sparse clone to the cone the deployment's config now asks for, as
|
|
10
|
+
* recorded beside the branch directories by the Lambda (branch-sparse.ts): a changed content
|
|
11
|
+
* root would otherwise leave every existing clone without its content. A full clone stays
|
|
12
|
+
* full. Best-effort per clone, like [SYNC-M2].
|
|
13
|
+
*
|
|
14
|
+
* The cone is compared without a lock, and changed only under the clone's provisioning lock
|
|
15
|
+
* and content-write lock, both zero-retry, outside a rebase: `sparse-checkout set` rewrites the
|
|
16
|
+
* working tree. A clone that is busy, or holds an unpublished deletion, is retried next cycle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reapplySparseCones(ctx: {
|
|
19
|
+
contentBranchesPath: string;
|
|
20
|
+
}): Promise<SparseConeReport>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readRecordedSparseCone, sameCone } from '../branch-sparse.js';
|
|
4
|
+
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
5
|
+
import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
|
|
6
|
+
import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
7
|
+
import { isRebaseInProgress } from '../utils/git.js';
|
|
8
|
+
import { workerLog, workerLogWarn } from './log.js';
|
|
9
|
+
import { holdProvisionedWorkspace, releaseProvisionedWorkspace } from './provisioned-workspace.js';
|
|
10
|
+
import { assertSharedRepoConfig, sharedRepoGit } from './shared-repo-git.js';
|
|
11
|
+
/** A clone's cone directories, or null when it is not sparse (or cannot be read). */
|
|
12
|
+
async function currentCone(git) {
|
|
13
|
+
try {
|
|
14
|
+
const listed = (await git.raw(['sparse-checkout', 'list'])).trim();
|
|
15
|
+
return listed ? listed.split('\n') : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Move each provisioned sparse clone to the cone the deployment's config now asks for, as
|
|
23
|
+
* recorded beside the branch directories by the Lambda (branch-sparse.ts): a changed content
|
|
24
|
+
* root would otherwise leave every existing clone without its content. A full clone stays
|
|
25
|
+
* full. Best-effort per clone, like [SYNC-M2].
|
|
26
|
+
*
|
|
27
|
+
* The cone is compared without a lock, and changed only under the clone's provisioning lock
|
|
28
|
+
* and content-write lock, both zero-retry, outside a rebase: `sparse-checkout set` rewrites the
|
|
29
|
+
* working tree. A clone that is busy, or holds an unpublished deletion, is retried next cycle.
|
|
30
|
+
*/
|
|
31
|
+
export async function reapplySparseCones(ctx) {
|
|
32
|
+
const report = { reapplied: [], failed: [] };
|
|
33
|
+
const recorded = await readRecordedSparseCone(ctx.contentBranchesPath);
|
|
34
|
+
if (!recorded)
|
|
35
|
+
return report;
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await fs.readdir(ctx.contentBranchesPath, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (isNodeError(err) && err.code === 'ENOENT')
|
|
42
|
+
return report;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
const dirNames = entries
|
|
46
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
49
|
+
for (const dirName of dirNames) {
|
|
50
|
+
const branchPath = path.join(ctx.contentBranchesPath, dirName);
|
|
51
|
+
// Without its own .git, git would answer for a repository above the branches root.
|
|
52
|
+
const isClone = await fs.stat(path.join(branchPath, '.git')).then((stat) => stat.isDirectory(), () => false);
|
|
53
|
+
if (!isClone)
|
|
54
|
+
continue;
|
|
55
|
+
const git = sharedRepoGit(branchPath, 'worktree');
|
|
56
|
+
// Read before this clone's config check, so a read that never finishes (an include naming a
|
|
57
|
+
// pipe) must not hold the sync cycle.
|
|
58
|
+
const cone = await currentCone(sharedRepoGit(branchPath, 'worktree', { timeout: { block: 30_000 } }));
|
|
59
|
+
if (cone === null || sameCone(cone, recorded.cone))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
// `sparse-checkout set` checks files out, which runs any filter driver the config names.
|
|
63
|
+
await assertSharedRepoConfig(branchPath, 'worktree');
|
|
64
|
+
const outcome = await reapplyOne(ctx.contentBranchesPath, dirName, git, recorded.cone);
|
|
65
|
+
if (outcome === 'applied') {
|
|
66
|
+
workerLog(` ${dirName}: sparse-checkout cone ${[...cone].sort().join(',')} -> ` +
|
|
67
|
+
(recorded.cone ? [...recorded.cone].sort().join(',') : 'full checkout'));
|
|
68
|
+
report.reapplied.push(dirName);
|
|
69
|
+
}
|
|
70
|
+
else if (typeof outcome === 'number') {
|
|
71
|
+
workerLog(` ${dirName}: sparse-checkout cone waits for ${outcome} unpublished ` +
|
|
72
|
+
`deletion${outcome === 1 ? '' : 's'} to be published`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const error = getErrorMessage(err);
|
|
77
|
+
workerLogWarn(` ${dirName}: could not change the sparse-checkout cone: ${error}`);
|
|
78
|
+
report.failed.push({ branch: dirName, error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return report;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply `target` under both locks: `'applied'`, `'skipped'` when the clone is busy or no longer
|
|
85
|
+
* differs, or the count of its unpublished deletions, which hold the cone where it is.
|
|
86
|
+
*/
|
|
87
|
+
async function reapplyOne(contentBranchesPath, dirName, git, target) {
|
|
88
|
+
const branchPath = path.join(contentBranchesPath, dirName);
|
|
89
|
+
const hold = await holdProvisionedWorkspace(contentBranchesPath, dirName);
|
|
90
|
+
if (hold.kind !== 'held')
|
|
91
|
+
return 'skipped';
|
|
92
|
+
try {
|
|
93
|
+
let releaseContent;
|
|
94
|
+
let contentLost = false;
|
|
95
|
+
try {
|
|
96
|
+
releaseContent = await tryAcquireContentWriteLock(branchPath, () => {
|
|
97
|
+
contentLost = true;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (isNodeError(err) && err.code === 'ELOCKED')
|
|
102
|
+
return 'skipped';
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
if (await isRebaseInProgress(branchPath))
|
|
107
|
+
return 'skipped';
|
|
108
|
+
const cone = await currentCone(git);
|
|
109
|
+
if (cone === null || sameCone(cone, target))
|
|
110
|
+
return 'skipped';
|
|
111
|
+
// A tracked file missing from the working tree is the one edit a cone change loses: leaving
|
|
112
|
+
// the cone marks it skip-worktree, so the deletion drops out of `status` and `add -A`, and a
|
|
113
|
+
// wider cone checks the file out again. Modified, untracked and staged changes all survive.
|
|
114
|
+
const deleted = (await git.raw(['ls-files', '--deleted', '-z'])).split('\0').filter(Boolean);
|
|
115
|
+
if (deleted.length > 0)
|
|
116
|
+
return deleted.length;
|
|
117
|
+
if (hold.isCompromised() || contentLost)
|
|
118
|
+
return 'skipped';
|
|
119
|
+
try {
|
|
120
|
+
await git.raw(target
|
|
121
|
+
? ['sparse-checkout', 'set', '--cone', '--', ...target]
|
|
122
|
+
: ['sparse-checkout', 'disable']);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await invalidateBranchContentCaches(branchPath);
|
|
126
|
+
}
|
|
127
|
+
return 'applied';
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await releaseContent().catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await releaseProvisionedWorkspace(hold.release, dirName);
|
|
135
|
+
}
|
|
136
|
+
}
|