canopycms 0.0.67-int.90 → 0.0.67
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/README.md +30 -3
- package/dist/ai/generate.js +1 -27
- package/dist/ai/handler.d.ts +0 -4
- package/dist/ai/handler.js +0 -11
- package/dist/ai/index.d.ts +1 -8
- package/dist/ai/index.js +1 -8
- package/dist/ai/json-to-markdown.js +15 -58
- package/dist/ai/resolve-branch.d.ts +2 -7
- package/dist/ai/resolve-branch.js +2 -8
- package/dist/ai/strip-mdx.d.ts +5 -13
- package/dist/ai/strip-mdx.js +5 -17
- package/dist/ai/to-plain-text.d.ts +10 -28
- package/dist/ai/to-plain-text.js +10 -28
- package/dist/ai/transform-components.d.ts +2 -4
- package/dist/ai/transform-components.js +8 -19
- package/dist/ai/types.d.ts +5 -10
- package/dist/api/admin-branch-health.d.ts +4 -12
- package/dist/api/admin-branch-health.js +26 -66
- package/dist/api/admin.d.ts +4 -12
- package/dist/api/admin.js +10 -26
- package/dist/api/assets.d.ts +0 -25
- package/dist/api/assets.js +50 -91
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +0 -2
- package/dist/api/branch-review.d.ts +0 -8
- package/dist/api/branch-review.js +0 -13
- package/dist/api/branch-status.d.ts +0 -3
- package/dist/api/branch-status.js +8 -33
- package/dist/api/branch-withdraw.d.ts +0 -1
- package/dist/api/branch-withdraw.js +6 -9
- package/dist/api/branch.d.ts +7 -12
- package/dist/api/branch.js +7 -44
- package/dist/api/client.d.ts +71 -224
- package/dist/api/client.js +82 -233
- package/dist/api/comments.d.ts +2 -4
- package/dist/api/comments.js +0 -17
- package/dist/api/content.d.ts +0 -6
- package/dist/api/content.js +66 -142
- package/dist/api/entries-constants.d.ts +5 -9
- package/dist/api/entries-constants.js +5 -9
- package/dist/api/entries.d.ts +8 -13
- package/dist/api/entries.js +20 -51
- package/dist/api/github-sync.d.ts +2 -4
- package/dist/api/github-sync.js +4 -8
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.js +8 -21
- package/dist/api/guards.d.ts +4 -16
- package/dist/api/guards.js +3 -22
- package/dist/api/index.d.ts +4 -4
- package/dist/api/permissions.d.ts +2 -0
- package/dist/api/permissions.js +18 -40
- package/dist/api/reference-options.d.ts +0 -1
- package/dist/api/reference-options.js +3 -11
- package/dist/api/request-body-hash.d.ts +12 -23
- package/dist/api/request-body-hash.js +12 -23
- package/dist/api/resolve-references.d.ts +0 -1
- package/dist/api/resolve-references.js +13 -28
- package/dist/api/route-builder.d.ts +3 -7
- package/dist/api/route-builder.js +1 -1
- package/dist/api/routes.d.ts +12 -0
- package/dist/api/routes.js +47 -0
- package/dist/api/schema.d.ts +23 -33
- package/dist/api/schema.js +20 -74
- package/dist/api/settings-helpers.js +4 -5
- package/dist/api/types.d.ts +0 -5
- package/dist/api/user.js +0 -5
- package/dist/api/validators.d.ts +14 -52
- package/dist/api/validators.js +14 -52
- package/dist/assets/asset-prefixes.d.ts +4 -8
- package/dist/assets/asset-prefixes.js +4 -8
- package/dist/assets/asset-src.d.ts +5 -22
- package/dist/assets/asset-src.js +5 -21
- package/dist/assets/factory.d.ts +1 -2
- package/dist/assets/factory.js +1 -2
- package/dist/assets/finalize.d.ts +3 -1
- package/dist/assets/finalize.js +3 -1
- package/dist/assets/index.d.ts +3 -3
- package/dist/assets/index.js +1 -1
- package/dist/assets/keys.d.ts +5 -3
- package/dist/assets/keys.js +6 -3
- package/dist/assets/pipeline.d.ts +3 -2
- package/dist/assets/pipeline.js +22 -56
- package/dist/assets/sharp-loader.d.ts +2 -10
- package/dist/assets/sharp-loader.js +2 -10
- package/dist/assets/store-local.d.ts +1 -2
- package/dist/assets/store-local.js +1 -2
- package/dist/assets/store-s3.d.ts +3 -4
- package/dist/assets/svg-sanitizer.d.ts +6 -14
- package/dist/assets/svg-sanitizer.js +6 -14
- package/dist/assets/transform-directives.d.ts +3 -4
- package/dist/assets/transform-directives.js +1 -1
- package/dist/assets/transform.d.ts +19 -30
- package/dist/assets/transform.js +18 -31
- package/dist/auth/cache.d.ts +2 -3
- package/dist/auth/cache.js +2 -3
- package/dist/auth/caching-auth-plugin.d.ts +11 -22
- package/dist/auth/caching-auth-plugin.js +12 -14
- package/dist/auth/context-helpers.d.ts +6 -20
- package/dist/auth/context-helpers.js +5 -17
- package/dist/auth/file-based-auth-cache.d.ts +10 -21
- package/dist/auth/file-based-auth-cache.js +23 -51
- package/dist/auth/plugin.d.ts +29 -61
- package/dist/auth/plugin.js +9 -13
- package/dist/auth/types.d.ts +11 -20
- package/dist/authorization/branch.d.ts +30 -49
- package/dist/authorization/branch.js +16 -42
- package/dist/authorization/content.d.ts +11 -40
- package/dist/authorization/content.js +13 -45
- package/dist/authorization/groups/index.d.ts +0 -3
- package/dist/authorization/groups/index.js +0 -3
- package/dist/authorization/groups/loader.d.ts +14 -28
- package/dist/authorization/groups/loader.js +14 -38
- package/dist/authorization/groups/schema.d.ts +0 -6
- package/dist/authorization/groups/schema.js +4 -7
- package/dist/authorization/helpers.d.ts +9 -28
- package/dist/authorization/helpers.js +9 -28
- package/dist/authorization/index.d.ts +3 -25
- package/dist/authorization/index.js +3 -35
- package/dist/authorization/path.d.ts +6 -13
- package/dist/authorization/path.js +8 -25
- package/dist/authorization/permissions/index.d.ts +0 -3
- package/dist/authorization/permissions/index.js +0 -3
- package/dist/authorization/permissions/loader.d.ts +14 -27
- package/dist/authorization/permissions/loader.js +14 -32
- package/dist/authorization/permissions/schema.d.ts +1 -6
- package/dist/authorization/permissions/schema.js +6 -13
- package/dist/authorization/protected-branch.d.ts +36 -65
- package/dist/authorization/protected-branch.js +29 -41
- package/dist/authorization/settings-file-store.d.ts +38 -81
- package/dist/authorization/settings-file-store.js +38 -79
- package/dist/authorization/types.d.ts +4 -26
- package/dist/authorization/types.js +0 -6
- package/dist/authorization/validation.d.ts +1 -23
- package/dist/authorization/validation.js +1 -24
- package/dist/branch-health.d.ts +39 -67
- package/dist/branch-health.js +27 -43
- package/dist/branch-metadata-file.d.ts +16 -32
- package/dist/branch-metadata-file.js +13 -26
- package/dist/branch-metadata.d.ts +49 -80
- package/dist/branch-metadata.js +57 -87
- package/dist/branch-registry.d.ts +26 -64
- package/dist/branch-registry.js +44 -93
- package/dist/branch-schema-cache.d.ts +34 -124
- package/dist/branch-schema-cache.js +49 -155
- package/dist/branch-workspace.d.ts +4 -4
- package/dist/branch-workspace.js +15 -19
- package/dist/build/generate-ai-content.d.ts +1 -0
- package/dist/build/generate-ai-content.js +12 -25
- package/dist/build-canopy.d.ts +17 -27
- package/dist/build-canopy.js +16 -23
- package/dist/build-mode.d.ts +25 -27
- package/dist/build-mode.js +25 -29
- package/dist/cli/cli.d.ts +15 -26
- package/dist/cli/cli.js +601 -978
- package/dist/cli/generate-ai-content.js +351 -628
- package/dist/cli/github-app-manifest.d.ts +84 -0
- package/dist/cli/github-app-manifest.js +161 -0
- package/dist/cli/init-github-app.d.ts +110 -444
- package/dist/cli/init-github-app.js +236 -773
- package/dist/cli/init.js +39 -80
- package/dist/cli/migrate.d.ts +13 -10
- package/dist/cli/migrate.js +22 -30
- package/dist/cli/project-detect.d.ts +17 -31
- package/dist/cli/project-detect.js +31 -42
- package/dist/cli/project-root.d.ts +4 -11
- package/dist/cli/project-root.js +4 -11
- package/dist/cli/prompt.d.ts +10 -0
- package/dist/cli/prompt.js +67 -0
- package/dist/cli/sync.d.ts +0 -8
- package/dist/cli/sync.js +8 -28
- package/dist/cli/template-files/middleware.ts.template +15 -29
- package/dist/cli/templates.d.ts +2 -12
- package/dist/cli/templates.js +4 -4
- package/dist/client.d.ts +1 -1
- package/dist/comment-store.d.ts +32 -53
- package/dist/comment-store.js +32 -52
- package/dist/config/flatten.d.ts +0 -3
- package/dist/config/flatten.js +2 -12
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -4
- package/dist/config/schemas/collection.d.ts +0 -8
- package/dist/config/schemas/collection.js +0 -10
- package/dist/config/schemas/config.d.ts +6 -78
- package/dist/config/schemas/config.js +38 -54
- package/dist/config/schemas/field.d.ts +2 -242
- package/dist/config/schemas/field.js +9 -20
- package/dist/config/schemas/media.js +1 -1
- package/dist/config/schemas/permissions.js +0 -2
- package/dist/config/schemas/url.d.ts +5 -6
- package/dist/config/schemas/url.js +5 -6
- package/dist/config/types.d.ts +45 -33
- package/dist/config/types.js +0 -1
- package/dist/config.d.ts +1 -8
- package/dist/config.js +1 -9
- package/dist/content-id-index.d.ts +76 -188
- package/dist/content-id-index.js +79 -230
- package/dist/content-index-generation.d.ts +27 -48
- package/dist/content-index-generation.js +35 -84
- package/dist/content-index-registry.d.ts +15 -24
- package/dist/content-index-registry.js +5 -9
- package/dist/content-listing.d.ts +64 -107
- package/dist/content-listing.js +68 -118
- package/dist/content-reader.d.ts +20 -29
- package/dist/content-reader.js +10 -18
- package/dist/content-store.d.ts +210 -336
- package/dist/content-store.js +413 -657
- package/dist/content-tree.d.ts +44 -86
- package/dist/content-tree.js +26 -54
- package/dist/context.d.ts +67 -94
- package/dist/context.js +53 -72
- package/dist/dev-content-watcher.d.ts +18 -18
- package/dist/dev-content-watcher.js +41 -44
- package/dist/editor/BranchManager.d.ts +3 -6
- package/dist/editor/BranchManager.js +24 -44
- package/dist/editor/CanopyEditor.js +5 -2
- package/dist/editor/CommentsPanel.js +0 -2
- package/dist/editor/Editor.js +19 -76
- package/dist/editor/EditorAuthGate.d.ts +27 -0
- package/dist/editor/EditorAuthGate.js +140 -0
- package/dist/editor/EntryNavigator.js +0 -20
- package/dist/editor/FormRenderer.d.ts +1 -2
- package/dist/editor/FormRenderer.js +8 -23
- package/dist/editor/admin/SystemHealthPanel.js +20 -37
- package/dist/editor/admin/useSystemHealth.d.ts +2 -3
- package/dist/editor/admin/useSystemHealth.js +10 -12
- package/dist/editor/client-reference-resolver.d.ts +2 -5
- package/dist/editor/client-reference-resolver.js +2 -19
- package/dist/editor/comments/BranchComments.js +0 -4
- package/dist/editor/comments/EntryComments.js +0 -4
- package/dist/editor/comments/FieldWrapper.js +0 -4
- package/dist/editor/comments/InlineCommentThread.js +0 -2
- package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
- package/dist/editor/comments/ThreadCarousel.js +0 -12
- package/dist/editor/components/EditorFooter.d.ts +0 -8
- package/dist/editor/components/EditorFooter.js +0 -5
- package/dist/editor/components/EditorHeader.d.ts +0 -31
- package/dist/editor/components/EditorHeader.js +21 -71
- package/dist/editor/components/EditorSidebar.d.ts +0 -19
- package/dist/editor/components/EditorSidebar.js +0 -16
- package/dist/editor/components/EntryCreateModal.d.ts +4 -4
- package/dist/editor/components/EntryCreateModal.js +7 -22
- package/dist/editor/components/RenameEntryModal.js +0 -4
- package/dist/editor/components/UserBadge.js +0 -10
- package/dist/editor/components/index.js +0 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -7
- package/dist/editor/context/ApiClientContext.js +39 -11
- package/dist/editor/context/AssetContext.d.ts +7 -17
- package/dist/editor/context/AssetContext.js +7 -17
- package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
- package/dist/editor/context/EditorIdentityContext.js +7 -0
- package/dist/editor/context/EditorStateContext.d.ts +10 -41
- package/dist/editor/context/EditorStateContext.js +7 -23
- package/dist/editor/context/SWRProvider.d.ts +3 -6
- package/dist/editor/context/index.d.ts +1 -20
- package/dist/editor/context/index.js +1 -20
- package/dist/editor/editor-utils.d.ts +8 -47
- package/dist/editor/editor-utils.js +19 -72
- package/dist/editor/fields/BlockField.d.ts +2 -2
- package/dist/editor/fields/BlockField.js +1 -2
- package/dist/editor/fields/CodeField.d.ts +0 -1
- package/dist/editor/fields/CodeField.js +0 -1
- package/dist/editor/fields/DateTimeField.d.ts +15 -22
- package/dist/editor/fields/DateTimeField.js +17 -26
- package/dist/editor/fields/ImageField.d.ts +2 -2
- package/dist/editor/fields/ImageField.js +8 -14
- package/dist/editor/fields/InlineGroupField.d.ts +0 -1
- package/dist/editor/fields/InlineGroupField.js +0 -1
- package/dist/editor/fields/MarkdownField.d.ts +0 -1
- package/dist/editor/fields/MarkdownField.js +6 -11
- package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
- package/dist/editor/fields/MdxImageDialog.js +0 -1
- package/dist/editor/fields/NumberField.d.ts +9 -19
- package/dist/editor/fields/NumberField.js +11 -22
- package/dist/editor/fields/NumberListField.d.ts +7 -10
- package/dist/editor/fields/NumberListField.js +7 -10
- package/dist/editor/fields/ObjectField.d.ts +5 -8
- package/dist/editor/fields/ObjectField.js +0 -1
- package/dist/editor/fields/ReferenceField.d.ts +1 -1
- package/dist/editor/fields/ReferenceField.js +11 -16
- package/dist/editor/fields/SelectField.d.ts +2 -2
- package/dist/editor/fields/SelectField.js +0 -1
- package/dist/editor/fields/StringListField.d.ts +0 -4
- package/dist/editor/fields/StringListField.js +4 -9
- package/dist/editor/fields/TextField.d.ts +0 -1
- package/dist/editor/fields/TextField.js +0 -1
- package/dist/editor/fields/ToggleField.d.ts +0 -1
- package/dist/editor/fields/ToggleField.js +0 -1
- package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
- package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
- package/dist/editor/group-manager/GroupCard.d.ts +0 -3
- package/dist/editor/group-manager/GroupForm.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
- package/dist/editor/group-manager/MemberList.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
- package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
- package/dist/editor/group-manager/index.d.ts +0 -6
- package/dist/editor/group-manager/index.js +0 -10
- package/dist/editor/group-manager/types.d.ts +2 -5
- package/dist/editor/group-manager/types.js +0 -3
- package/dist/editor/hooks/index.d.ts +6 -11
- package/dist/editor/hooks/index.js +6 -12
- package/dist/editor/hooks/useBranchActions.d.ts +0 -14
- package/dist/editor/hooks/useBranchActions.js +0 -22
- package/dist/editor/hooks/useBranchManager.d.ts +2 -48
- package/dist/editor/hooks/useBranchManager.js +4 -64
- package/dist/editor/hooks/useBranchesData.d.ts +7 -6
- package/dist/editor/hooks/useBranchesData.js +7 -6
- package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
- package/dist/editor/hooks/useCommentSystem.js +1 -36
- package/dist/editor/hooks/useDraftManager.d.ts +2 -35
- package/dist/editor/hooks/useDraftManager.js +23 -64
- package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
- package/dist/editor/hooks/useEditorLayout.js +0 -23
- package/dist/editor/hooks/useEntriesData.d.ts +1 -0
- package/dist/editor/hooks/useEntriesData.js +2 -3
- package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
- package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
- package/dist/editor/hooks/useEntryManager.d.ts +2 -27
- package/dist/editor/hooks/useEntryManager.js +29 -78
- package/dist/editor/hooks/useGroupManager.d.ts +0 -21
- package/dist/editor/hooks/useGroupManager.js +0 -21
- package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
- package/dist/editor/hooks/usePermissionManager.js +0 -20
- package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
- package/dist/editor/hooks/useReferenceResolution.js +1 -48
- package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
- package/dist/editor/hooks/useSchemaManager.js +0 -19
- package/dist/editor/hooks/useUserContext.d.ts +2 -9
- package/dist/editor/hooks/useUserContext.js +10 -10
- package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
- package/dist/editor/hooks/useUserMetadata.js +0 -3
- package/dist/editor/media/AssetCard.d.ts +0 -1
- package/dist/editor/media/AssetCard.js +1 -2
- package/dist/editor/media/CropStep.d.ts +0 -1
- package/dist/editor/media/CropStep.js +0 -1
- package/dist/editor/media/MediaLibrary.d.ts +1 -2
- package/dist/editor/media/MediaLibrary.js +1 -2
- package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
- package/dist/editor/media/MediaLibraryBody.js +0 -1
- package/dist/editor/media/crop-math.d.ts +1 -1
- package/dist/editor/media/crop-math.js +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
- package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
- package/dist/editor/permission-manager/PermissionEditor.js +0 -1
- package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
- package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
- package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
- package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
- package/dist/editor/permission-manager/constants.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
- package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
- package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
- package/dist/editor/permission-manager/index.d.ts +0 -7
- package/dist/editor/permission-manager/index.js +0 -16
- package/dist/editor/permission-manager/types.d.ts +2 -12
- package/dist/editor/permission-manager/types.js +0 -3
- package/dist/editor/permission-manager/utils.d.ts +0 -28
- package/dist/editor/permission-manager/utils.js +9 -23
- package/dist/editor/preview-bridge.d.ts +5 -1
- package/dist/editor/preview-bridge.js +4 -4
- package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
- package/dist/editor/schema-editor/CollectionEditor.js +9 -31
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
- package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
- package/dist/editor/schema-editor/index.d.ts +0 -7
- package/dist/editor/schema-editor/index.js +0 -7
- package/dist/editor/utils/env.d.ts +3 -18
- package/dist/editor/utils/env.js +6 -25
- package/dist/entry-link-resolver.d.ts +13 -35
- package/dist/entry-link-resolver.js +20 -53
- package/dist/entry-schema-registry.d.ts +20 -60
- package/dist/entry-schema-registry.js +20 -69
- package/dist/entry-schema.d.ts +118 -223
- package/dist/entry-schema.js +46 -91
- package/dist/git-manager.d.ts +101 -200
- package/dist/git-manager.js +211 -384
- package/dist/github-service.d.ts +34 -75
- package/dist/github-service.js +37 -91
- package/dist/http/handler.d.ts +6 -37
- package/dist/http/handler.js +65 -120
- package/dist/http/router.d.ts +15 -32
- package/dist/http/router.js +34 -99
- package/dist/http/types.d.ts +15 -40
- package/dist/http/types.js +3 -7
- package/dist/id.d.ts +4 -10
- package/dist/id.js +4 -10
- package/dist/index.js +6 -9
- package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
- package/dist/operating-mode/client-safe-strategy.js +8 -28
- package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
- package/dist/operating-mode/client-unsafe-strategy.js +9 -34
- package/dist/operating-mode/client.d.ts +3 -10
- package/dist/operating-mode/client.js +3 -8
- package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
- package/dist/operating-mode/deployment-name-fixtures.js +14 -18
- package/dist/operating-mode/deployment-name.d.ts +19 -29
- package/dist/operating-mode/deployment-name.js +29 -47
- package/dist/operating-mode/index.d.ts +4 -25
- package/dist/operating-mode/index.js +9 -31
- package/dist/operating-mode/mode-env.d.ts +32 -54
- package/dist/operating-mode/mode-env.js +36 -58
- package/dist/operating-mode/types.d.ts +24 -72
- package/dist/operating-mode/types.js +1 -7
- package/dist/paths/branch-name.d.ts +30 -40
- package/dist/paths/branch-name.js +30 -40
- package/dist/paths/branch.d.ts +4 -18
- package/dist/paths/branch.js +6 -22
- package/dist/paths/index.d.ts +2 -13
- package/dist/paths/index.js +3 -23
- package/dist/paths/normalize-server.d.ts +3 -15
- package/dist/paths/normalize-server.js +3 -15
- package/dist/paths/normalize.d.ts +13 -33
- package/dist/paths/normalize.js +13 -33
- package/dist/paths/resolve.d.ts +4 -17
- package/dist/paths/resolve.js +4 -22
- package/dist/paths/types.d.ts +12 -34
- package/dist/paths/types.js +5 -7
- package/dist/paths/validation.d.ts +22 -119
- package/dist/paths/validation.js +30 -148
- package/dist/reference-resolver.d.ts +0 -13
- package/dist/reference-resolver.js +0 -18
- package/dist/resolve-canopy-user.d.ts +16 -18
- package/dist/resolve-canopy-user.js +17 -26
- package/dist/resource-generation.d.ts +16 -25
- package/dist/resource-generation.js +34 -97
- package/dist/schema/meta-loader.d.ts +2 -14
- package/dist/schema/meta-loader.js +15 -64
- package/dist/schema/resolver.d.ts +1 -4
- package/dist/schema/resolver.js +1 -6
- package/dist/schema/schema-store-types.d.ts +0 -12
- package/dist/schema/schema-store.d.ts +61 -138
- package/dist/schema/schema-store.js +85 -282
- package/dist/schema/types.d.ts +0 -4
- package/dist/server.d.ts +62 -93
- package/dist/server.js +63 -94
- package/dist/services.d.ts +23 -47
- package/dist/services.js +55 -98
- package/dist/settings-workspace.d.ts +17 -30
- package/dist/settings-workspace.js +51 -81
- package/dist/static/index.d.ts +26 -52
- package/dist/static/index.js +33 -67
- package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
- package/dist/task-queue/cms-task-queue.js +13 -0
- package/dist/task-queue/task-queue-config.d.ts +3 -0
- package/dist/{worker → task-queue}/task-queue-config.js +1 -6
- package/dist/task-queue/task-queue.d.ts +16 -22
- package/dist/task-queue/task-queue.js +20 -44
- package/dist/task-queue/types.d.ts +0 -1
- package/dist/task-queue/worker-status.d.ts +35 -0
- package/dist/task-queue/worker-status.js +40 -0
- package/dist/types.d.ts +40 -51
- package/dist/url-collision.d.ts +26 -40
- package/dist/url-collision.js +47 -63
- package/dist/url-exclusivity-fixtures.d.ts +15 -20
- package/dist/url-exclusivity-fixtures.js +14 -19
- package/dist/url-path-resolver.d.ts +8 -16
- package/dist/url-path-resolver.js +19 -29
- package/dist/user.d.ts +8 -21
- package/dist/user.js +10 -19
- package/dist/utils/async-mutex.js +4 -6
- package/dist/utils/atomic-write.d.ts +2 -11
- package/dist/utils/atomic-write.js +2 -11
- package/dist/utils/body-field.d.ts +8 -15
- package/dist/utils/body-field.js +8 -15
- package/dist/utils/content-serialize.d.ts +18 -24
- package/dist/utils/content-serialize.js +109 -160
- package/dist/utils/content-write-lock.d.ts +51 -106
- package/dist/utils/content-write-lock.js +66 -111
- package/dist/utils/debug.d.ts +7 -23
- package/dist/utils/debug.js +7 -23
- package/dist/utils/entry-url.d.ts +11 -23
- package/dist/utils/entry-url.js +11 -26
- package/dist/utils/error.d.ts +25 -90
- package/dist/utils/error.js +60 -147
- package/dist/utils/flatten-group-fields.d.ts +5 -7
- package/dist/utils/flatten-group-fields.js +5 -7
- package/dist/utils/format.d.ts +1 -5
- package/dist/utils/format.js +1 -5
- package/dist/utils/fs.d.ts +3 -4
- package/dist/utils/fs.js +3 -4
- package/dist/utils/git.d.ts +52 -83
- package/dist/utils/git.js +79 -125
- package/dist/utils/logger.d.ts +35 -50
- package/dist/utils/logger.js +35 -52
- package/dist/utils/provisioning-lock.d.ts +23 -35
- package/dist/utils/provisioning-lock.js +49 -69
- package/dist/utils/sanitize-href.d.ts +5 -10
- package/dist/utils/sanitize-href.js +8 -18
- package/dist/utils/title-field.d.ts +17 -20
- package/dist/utils/title-field.js +18 -27
- package/dist/utils/typed-filename.d.ts +20 -48
- package/dist/utils/typed-filename.js +25 -61
- package/dist/utils/url-prefix.d.ts +3 -3
- package/dist/utils/url-prefix.js +3 -3
- package/dist/validation/block-structural-keys.d.ts +16 -31
- package/dist/validation/block-structural-keys.js +19 -29
- package/dist/validation/deletion-checker.d.ts +0 -34
- package/dist/validation/deletion-checker.js +1 -43
- package/dist/validation/entry-link-validator.d.ts +6 -13
- package/dist/validation/entry-link-validator.js +4 -13
- package/dist/validation/entry-type-reference-validator.d.ts +13 -18
- package/dist/validation/entry-type-reference-validator.js +13 -18
- package/dist/validation/entry-validator.d.ts +13 -25
- package/dist/validation/entry-validator.js +15 -32
- package/dist/validation/field-traversal.d.ts +3 -30
- package/dist/validation/field-traversal.js +1 -32
- package/dist/validation/reference-validator.d.ts +5 -24
- package/dist/validation/reference-validator.js +5 -30
- package/dist/worker/cms-worker.d.ts +172 -232
- package/dist/worker/cms-worker.js +241 -333
- package/dist/worker/git-sync.d.ts +32 -133
- package/dist/worker/git-sync.js +129 -191
- package/dist/worker/github-auth.d.ts +13 -19
- package/dist/worker/github-auth.js +13 -19
- package/dist/worker/history-rewrite.d.ts +53 -66
- package/dist/worker/history-rewrite.js +54 -60
- package/dist/worker/log.d.ts +24 -41
- package/dist/worker/log.js +25 -42
- package/dist/worker/rebase.d.ts +20 -110
- package/dist/worker/rebase.js +279 -354
- package/dist/worker/task-runner.d.ts +32 -66
- package/dist/worker/task-runner.js +126 -162
- package/dist/worker/worker-context.d.ts +41 -81
- package/package.json +4 -4
- package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
- package/dist/worker/task-queue-config.d.ts +0 -8
- package/dist/worker/task-queue.js +0 -19
- package/dist/worker/worker-status.d.ts +0 -47
- package/dist/worker/worker-status.js +0 -52
package/dist/worker/rebase.js
CHANGED
|
@@ -12,16 +12,15 @@ import { isRebaseInProgress } from '../utils/git.js';
|
|
|
12
12
|
import { enqueueGitHubPush, forcePublishToLocalRemote, markHistoryRewritten, readPublishedSha, reconcilePendingRewrite, } from './history-rewrite.js';
|
|
13
13
|
import { workerLog, workerLogWarn } from './log.js';
|
|
14
14
|
/**
|
|
15
|
-
* Poll GitHub for a submitted/approved branch's PR resolution.
|
|
15
|
+
* Poll GitHub for a submitted/approved branch's PR resolution. Those branches
|
|
16
|
+
* sit outside the rebase loop and get no other signal that their PR resolved --
|
|
17
|
+
* nothing pushes a merge/close webhook back into the branch workspace.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* merge
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* metadata). closed-without-merge -> record pullRequestState only; an
|
|
23
|
-
* admin decides the workflow transition from there. Best-effort: any
|
|
24
|
-
* failure here is logged and swallowed, retried next sync cycle.
|
|
19
|
+
* merged -> auto-archive via buildMergedBranchUpdate, shared with the manual
|
|
20
|
+
* markAsMerged API so both paths produce identical archived-branch metadata.
|
|
21
|
+
* closed-without-merge -> record pullRequestState only; an admin decides the
|
|
22
|
+
* workflow transition. Best-effort: a failure is logged, swallowed and retried
|
|
23
|
+
* next sync cycle.
|
|
25
24
|
*/
|
|
26
25
|
export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
|
|
27
26
|
const prNumber = metaFile?.branch.pullRequestNumber;
|
|
@@ -38,8 +37,8 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
|
|
|
38
37
|
});
|
|
39
38
|
if (data.merged) {
|
|
40
39
|
const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
|
|
41
|
-
//
|
|
42
|
-
// falls back to "now"
|
|
40
|
+
// GitHub's actual merge time when available; buildMergedBranchUpdate
|
|
41
|
+
// falls back to "now" when merged_at is absent.
|
|
43
42
|
await meta.save({
|
|
44
43
|
branch: buildMergedBranchUpdate(branchDir, data.merged_at ? new Date(data.merged_at) : undefined),
|
|
45
44
|
});
|
|
@@ -47,9 +46,8 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
|
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
48
|
const newState = data.state === 'closed' ? 'closed' : 'open';
|
|
50
|
-
// Re-load fresh
|
|
51
|
-
//
|
|
52
|
-
// landed since that snapshot was taken.
|
|
49
|
+
// Re-load fresh, not the `metaFile` snapshot passed in: a concurrent Lambda
|
|
50
|
+
// write (an editor re-submitting) may have landed since it was taken.
|
|
53
51
|
const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
54
52
|
if (currentMeta?.branch.pullRequestState === newState)
|
|
55
53
|
return;
|
|
@@ -66,29 +64,24 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
|
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
/**
|
|
69
|
-
* Persist a per-branch rebase failure to branch.json
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* reads), the same concern the `alreadyClean` no-op guard above exists
|
|
75
|
-
* for.
|
|
67
|
+
* Persist a per-branch rebase failure to branch.json, bounded to roughly one
|
|
68
|
+
* save per failing branch per hour: a branch stuck failing every cycle must not
|
|
69
|
+
* become save-per-cycle x N-failing-branches write amplification, since save()
|
|
70
|
+
* eager-regenerates the branch registry (branch-metadata.ts's
|
|
71
|
+
* invalidateRegistry(), O(branch count) EFS reads).
|
|
76
72
|
*
|
|
77
|
-
* Best-effort and non-fatal like every
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* so the whole method is wrapped, not just the load.
|
|
73
|
+
* Best-effort and non-fatal, like every metadata write in this loop's error
|
|
74
|
+
* paths: a corrupt branch.json, lock contention or any other save failure must
|
|
75
|
+
* never abort the per-branch iteration. One of the two call sites is inside the
|
|
76
|
+
* outer per-branch catch with no further catch around it, so the WHOLE method
|
|
77
|
+
* is wrapped, not just the load.
|
|
83
78
|
*/
|
|
84
|
-
|
|
79
|
+
async function recordRebaseFailure(ctx, branchPath, branchDir, message) {
|
|
85
80
|
const RECORD_REFRESH_MS = 60 * 60 * 1000; // 1 hour
|
|
86
|
-
// [REDACT] Defense
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// redactCredentials is idempotent, so redacting again here is free and
|
|
91
|
-
// keeps this method safe on its own.
|
|
81
|
+
// [REDACT] Defense in depth: rebaseFailure.message is persisted to
|
|
82
|
+
// branch.json and served to the browser by the branch-health admin endpoint.
|
|
83
|
+
// Both call sites already redact before passing in, but redactCredentials is
|
|
84
|
+
// idempotent, so redacting here is free and keeps this method safe alone.
|
|
92
85
|
const redactedMessage = redactCredentials(message);
|
|
93
86
|
try {
|
|
94
87
|
const existing = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
@@ -113,9 +106,8 @@ export async function recordRebaseFailure(ctx, branchPath, branchDir, message) {
|
|
|
113
106
|
});
|
|
114
107
|
}
|
|
115
108
|
catch (err) {
|
|
116
|
-
// Includes BranchMetadataCorruptError from the load above (a save()
|
|
117
|
-
//
|
|
118
|
-
// any other load/save failure -- recording is best-effort
|
|
109
|
+
// Includes BranchMetadataCorruptError from the load above (a save() against
|
|
110
|
+
// the same corrupt file would just throw again): recording is best-effort
|
|
119
111
|
// observability, never allowed to abort the branch loop.
|
|
120
112
|
workerLogWarn(` Failed to record rebase failure for ${branchDir}: ${getErrorMessage(err)}`);
|
|
121
113
|
}
|
|
@@ -126,56 +118,43 @@ const MAX_REBASE_ROUNDS = 50;
|
|
|
126
118
|
* Drive `git rebase` to a finished state, resolving each conflict in favour of
|
|
127
119
|
* the BRANCH's version, and report what happened.
|
|
128
120
|
*
|
|
129
|
-
* ABORT OWNERSHIP is split across
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* -
|
|
134
|
-
*
|
|
135
|
-
* - The **lock-compromised** exit leaves the rebase in progress, and is aborted
|
|
136
|
-
* by `rebaseOneBranch`'s OWN `contentLockCompromised && !completed` branch --
|
|
121
|
+
* ABORT OWNERSHIP is split across FOUR sites, none redundant; "the caller owns
|
|
122
|
+
* the abort" would be wrong in a way that invites deleting a live one:
|
|
123
|
+
* - the **unexpected-error** exit aborts HERE, before breaking, so the caller's
|
|
124
|
+
* `!completed` abort is a caught no-op for that path;
|
|
125
|
+
* - the **lock-compromised** exit leaves the rebase in progress and is aborted
|
|
126
|
+
* by `rebaseOneBranch`'s own `contentLockCompromised && !completed` branch,
|
|
137
127
|
* which returns `skippedLocked` before the `!completed` block below it is
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* before this one starts.)
|
|
147
|
-
*
|
|
148
|
-
* It can also THROW, from two places, and that is likewise covered rather than
|
|
149
|
-
* prevented: `branchGit.status()` is the first statement of the round catch, so
|
|
150
|
-
* a vanished `.git` or an EFS error escapes, as can the test hook beside it.
|
|
151
|
-
* Such a throw unwinds to `rebaseOneBranch`'s outer catch, and the last-resort
|
|
152
|
-
* abort in its `finally` -- the fourth site -- is what stops the clone being
|
|
153
|
-
* left mid-rebase. That `finally` is the backstop for exactly this, and is not
|
|
154
|
-
* dead code.
|
|
128
|
+
* reached -- a distinct site that cannot be folded into that one;
|
|
129
|
+
* - the **conflict-resolution-failure** and **MAX_REBASE_ROUNDS** exits are the
|
|
130
|
+
* only two the caller's `!completed` abort actually covers;
|
|
131
|
+
* - a THROW (`branchGit.status()` is the first statement of the round catch, so
|
|
132
|
+
* a vanished `.git` or an EFS error escapes, as can the test hook beside it)
|
|
133
|
+
* unwinds to `rebaseOneBranch`'s outer catch, where the last-resort abort in
|
|
134
|
+
* its `finally` is the backstop -- not dead code.
|
|
135
|
+
* (A fifth abort, in the interrupted-rebase recovery, cleans up a PREVIOUS run.)
|
|
155
136
|
*
|
|
156
137
|
* What this function DOES guarantee is narrower: no per-file resolution failure
|
|
157
|
-
* escapes it. `checkout --theirs` on a MODIFY/DELETE conflict
|
|
158
|
-
*
|
|
159
|
-
*
|
|
138
|
+
* escapes it. `checkout --theirs` on a MODIFY/DELETE conflict throws straight
|
|
139
|
+
* out of the round loop, skipping both abort sites and wedging the clone
|
|
140
|
+
* forever, so those set `failureReason` and break instead.
|
|
160
141
|
*
|
|
161
142
|
* `isLockCompromised` is re-read every round rather than passed as a value:
|
|
162
143
|
* [SYNC-C1] the content-write lock can be lost BETWEEN rounds, and
|
|
163
144
|
* `--continue`/`--skip` are as destructive as the initial rebase.
|
|
164
145
|
*/
|
|
165
146
|
async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLockCompromised) {
|
|
166
|
-
// Resolve-and-continue loop: keep branch version for conflicting files, then continue
|
|
167
|
-
// Non-conflicting files get main's changes; conflicting files keep branch version.
|
|
168
147
|
const conflictedFiles = [];
|
|
169
148
|
let nextAction = 'start';
|
|
170
149
|
let completed = false;
|
|
171
|
-
//
|
|
172
|
-
//
|
|
150
|
+
// Captured only on the "unexpected error" exit below, for the failed-summary
|
|
151
|
+
// entry pushed at the `if (!completed)` check.
|
|
173
152
|
let failureReason;
|
|
174
153
|
for (let round = 0; round < MAX_REBASE_ROUNDS && !completed; round++) {
|
|
175
|
-
// Re-checked every round: the compromise can land between rounds,
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
154
|
+
// Re-checked every round: the compromise can land between rounds, and
|
|
155
|
+
// `--continue`/`--skip` are as destructive as the initial rebase. Handled
|
|
156
|
+
// after the loop so it cannot be mistaken for the `!completed`
|
|
157
|
+
// rebase-FAILURE path below.
|
|
179
158
|
if (isLockCompromised())
|
|
180
159
|
break;
|
|
181
160
|
try {
|
|
@@ -197,30 +176,26 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
197
176
|
const st = await branchGit.status();
|
|
198
177
|
if (st.conflicted.length > 0) {
|
|
199
178
|
await ctx.afterConflictDetectedForTesting();
|
|
200
|
-
// During rebase, --theirs = the branch being replayed (editor's
|
|
201
|
-
//
|
|
179
|
+
// During rebase, --theirs = the branch being replayed (the editor's
|
|
180
|
+
// work): git rebase reverses ours/theirs, "ours" being the target.
|
|
202
181
|
//
|
|
203
|
-
// MODIFY/DELETE conflicts have no "their version" to check out
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// the
|
|
209
|
-
//
|
|
210
|
-
// index/working-tree code pair identifies which side deleted
|
|
211
|
-
// (verified against real git, not inferred):
|
|
182
|
+
// MODIFY/DELETE conflicts have no "their version" to check out and must
|
|
183
|
+
// be resolved by staging a delete or an add instead. `git checkout
|
|
184
|
+
// --theirs` on one exits non-zero and simple-git throws -- and because
|
|
185
|
+
// this loop body IS the round loop's catch, that throw escapes the
|
|
186
|
+
// round loop entirely, skipping BOTH `rebase --abort` sites below and
|
|
187
|
+
// wedging the clone mid-rebase forever. The index/working-tree code
|
|
188
|
+
// pair identifies which side deleted (verified against real git):
|
|
212
189
|
//
|
|
213
|
-
// U/D "deleted by them" -- the BRANCH deleted it, base
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// D/U "deleted by us" -- base deleted it, the BRANCH
|
|
217
|
-
//
|
|
218
|
-
//
|
|
190
|
+
// U/D "deleted by them" -- the BRANCH deleted it, base modified it,
|
|
191
|
+
// and git leaves base's version in the tree. Keeping the branch
|
|
192
|
+
// version means honouring the delete: git rm.
|
|
193
|
+
// D/U "deleted by us" -- base deleted it, the BRANCH modified it,
|
|
194
|
+
// and git leaves the branch's version in the tree. Keeping the
|
|
195
|
+
// branch version means keeping it: git add.
|
|
219
196
|
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// escaping -- deliberately NOT a rethrow, since a throw from
|
|
223
|
-
// here is exactly the bug being fixed.
|
|
197
|
+
// A per-file resolution that STILL fails routes into the `!completed`
|
|
198
|
+
// path below, which aborts and records, rather than escaping.
|
|
224
199
|
const conflictKind = new Map(st.files.map((f) => [f.path, `${f.index}${f.working_dir}`]));
|
|
225
200
|
let resolutionFailure;
|
|
226
201
|
for (const file of st.conflicted) {
|
|
@@ -247,8 +222,8 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
247
222
|
}
|
|
248
223
|
if (resolutionFailure !== undefined) {
|
|
249
224
|
// Same exit shape as the "unexpected error" branch below: set
|
|
250
|
-
// failureReason and break, letting the `!completed` block do
|
|
251
|
-
//
|
|
225
|
+
// failureReason and break, letting the `!completed` block do the
|
|
226
|
+
// single `rebase --abort` and record the failure once.
|
|
252
227
|
failureReason = resolutionFailure;
|
|
253
228
|
break;
|
|
254
229
|
}
|
|
@@ -262,11 +237,11 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
262
237
|
nextAction = 'skip';
|
|
263
238
|
}
|
|
264
239
|
else {
|
|
265
|
-
// Unexpected error — abort and leave branch behind.
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
240
|
+
// Unexpected error — abort and leave the branch behind.
|
|
241
|
+
// conflictStatus/conflictFiles are deliberately not updated: the
|
|
242
|
+
// rebase did not complete, so the true conflict state is unknown, and
|
|
243
|
+
// the existing (possibly stale) metadata is preserved until the next
|
|
244
|
+
// successful rebase cycle corrects it.
|
|
270
245
|
workerLogWarn(` Unexpected rebase error in ${branchDir}: ${msg || 'Unknown error'}`);
|
|
271
246
|
failureReason = msg || 'Unknown error';
|
|
272
247
|
await branchGit.rebase(['--abort']).catch(() => { });
|
|
@@ -279,38 +254,32 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
279
254
|
}
|
|
280
255
|
/**
|
|
281
256
|
* Map the file paths a rebase resolved --theirs onto the ContentIds the editor
|
|
282
|
-
* shows as conflicted, deduplicated.
|
|
257
|
+
* shows as conflicted, deduplicated. IDs rather than paths because they are
|
|
258
|
+
* immutable: a later slug rename must not orphan a conflict marker. Paths that
|
|
259
|
+
* carry no recoverable ID are dropped rather than guessed at.
|
|
283
260
|
*
|
|
284
|
-
*
|
|
285
|
-
* not orphan a conflict marker. Paths that carry no recoverable ID are dropped
|
|
286
|
-
* rather than guessed at.
|
|
287
|
-
*
|
|
288
|
-
* Pure -- the one piece of this loop that can be tested without a git repo.
|
|
261
|
+
* Pure.
|
|
289
262
|
*/
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// multi-segment contentRoot) has no embedded ID, so we use ROOT_COLLECTION_ID
|
|
297
|
-
// as a sentinel — but only for the configured contentRoot.
|
|
263
|
+
function conflictFilesToContentIds(conflictedFiles, contentRoot) {
|
|
264
|
+
// Entry files carry their ID in the filename. A `.collection.json` has none
|
|
265
|
+
// of its own (extractIdFromFilename returns null for dot-prefixed files), so
|
|
266
|
+
// the ID comes from the parent directory; the root content directory has no
|
|
267
|
+
// embedded ID at all, so ROOT_COLLECTION_ID stands in -- but only for the
|
|
268
|
+
// configured contentRoot.
|
|
298
269
|
//
|
|
299
|
-
// Two different notions of "parent" are needed
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// - `parentDir` (a basename) recovers a SUB-collection's own embedded ID
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// with either separator, so both sides go through normalizeFilesystemPath
|
|
313
|
-
// before comparing.
|
|
270
|
+
// Two different notions of "parent" are needed, and conflating them is the
|
|
271
|
+
// bug this guards against (see schema-store.ts's contentRootName doc comment
|
|
272
|
+
// for the same shape elsewhere):
|
|
273
|
+
// - `parentDir` (a basename) recovers a SUB-collection's own embedded ID,
|
|
274
|
+
// correct as a basename since a collection directory carries its ID in its
|
|
275
|
+
// own name, one path segment;
|
|
276
|
+
// - `parentPath` (the full normalized relative parent path) is what must be
|
|
277
|
+
// compared against `contentRoot`, which is documented (config/helpers.ts)
|
|
278
|
+
// as allowed to span multiple segments (e.g. "cms/content"). Comparing a
|
|
279
|
+
// basename against that full value is always false, silently dropping the
|
|
280
|
+
// root collection's conflict. Git reports POSIX paths and the configured
|
|
281
|
+
// value may use either separator, so both sides go through
|
|
282
|
+
// normalizeFilesystemPath first.
|
|
314
283
|
const normalizedContentRoot = normalizeFilesystemPath(contentRoot);
|
|
315
284
|
const conflictIds = [...new Set(conflictedFiles)]
|
|
316
285
|
.map((f) => {
|
|
@@ -336,44 +305,37 @@ export function conflictFilesToContentIds(conflictedFiles, contentRoot) {
|
|
|
336
305
|
* [SYNC-H1] Carry a just-rebased history forward into `remote.git`, and queue
|
|
337
306
|
* the GitHub hop.
|
|
338
307
|
*
|
|
339
|
-
* Only runs when the
|
|
308
|
+
* Only runs when the pre-rebase history had ALREADY been published
|
|
340
309
|
* (`publishedSha !== null`); otherwise nothing downstream knows about the
|
|
341
|
-
* commits the rebase replaced
|
|
342
|
-
*
|
|
310
|
+
* commits the rebase replaced. Without it, the editor's next submit is rejected
|
|
311
|
+
* non-fast-forward forever.
|
|
343
312
|
*
|
|
344
|
-
* `preRebaseHead` and `publishedSha` must
|
|
345
|
-
*
|
|
313
|
+
* `preRebaseHead` and `publishedSha` must BOTH be read before the rebase -- the
|
|
314
|
+
* clone's pre-rebase tip is exactly what the rebase destroys.
|
|
346
315
|
*/
|
|
347
316
|
async function carryForwardRewrittenHistory(ctx, args) {
|
|
348
317
|
const { branchPath, branchDir, branchRef, publishedSha, preRebaseHead } = args;
|
|
349
|
-
// [SYNC-H1] The rebase just rewrote this clone's history. If that
|
|
350
|
-
// history was already published, nothing else will ever reconcile
|
|
351
|
-
// remote.git (and GitHub) with it -- the editor's next submit would
|
|
352
|
-
// simply be rejected non-fast-forward. Carry the rewrite forward.
|
|
353
318
|
if (publishedSha !== null) {
|
|
354
319
|
if (publishedSha === preRebaseHead) {
|
|
355
|
-
// ARMING GUARD
|
|
356
|
-
//
|
|
357
|
-
//
|
|
320
|
+
// ARMING GUARD: remote.git holds EXACTLY what this clone just rebased
|
|
321
|
+
// away and nothing more, so a lease keyed to it can only undo our own
|
|
322
|
+
// rewrite.
|
|
358
323
|
//
|
|
359
|
-
// The inequality case below is not defensive padding
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// remote.git
|
|
367
|
-
//
|
|
368
|
-
// silently. Keying the lease to the pre-rebase tip turns that
|
|
369
|
-
// case into the visible divergence it should be.
|
|
324
|
+
// The inequality case below is not defensive padding. Branch clones never
|
|
325
|
+
// fetch their own branch (GitManager's clone is --single-branch and
|
|
326
|
+
// checkoutBranch only checks out an existing local branch), while
|
|
327
|
+
// reconcileTrackedBranches fast-forwards remote.git to GitHub's tip, so
|
|
328
|
+
// after a direct push of a fixup to the PR branch remote.git legitimately
|
|
329
|
+
// holds a commit this clone has never seen. Leasing on "whatever
|
|
330
|
+
// remote.git currently holds" would be SATISFIED there and would silently
|
|
331
|
+
// delete that fixup from remote.git and then GitHub. Keying the lease to
|
|
332
|
+
// the pre-rebase tip turns that case into the visible divergence it is.
|
|
370
333
|
//
|
|
371
|
-
// Order matters: mark, then push, then queue. A crash after any
|
|
372
|
-
//
|
|
373
|
-
// reconcilePendingRewrite() completes on a later cycle. Pushing
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// false "another deployment" diagnosis this change removes.
|
|
334
|
+
// Order matters: mark, then push, then queue. A crash after any step
|
|
335
|
+
// leaves the marker set with the work unfinished, which
|
|
336
|
+
// reconcilePendingRewrite() completes on a later cycle. Pushing first
|
|
337
|
+
// would leave remote.git rewritten, GitHub stale and nothing recorded --
|
|
338
|
+
// unrecoverable, and diagnosed as a false "another deployment" collision.
|
|
377
339
|
await markHistoryRewritten(ctx, branchPath, branchDir, publishedSha);
|
|
378
340
|
if (await forcePublishToLocalRemote(ctx, branchPath, branchRef, publishedSha)) {
|
|
379
341
|
await enqueueGitHubPush(ctx, branchRef);
|
|
@@ -401,9 +363,6 @@ async function carryForwardRewrittenHistory(ctx, args) {
|
|
|
401
363
|
* subprocesses against a shared filesystem.
|
|
402
364
|
*/
|
|
403
365
|
export async function runRebaseCycle(ctx) {
|
|
404
|
-
// PR-W1: collected across the loop below and returned as a summary
|
|
405
|
-
// (folded into worker-status.json by syncGit()). Purely additive
|
|
406
|
-
// bookkeeping -- doesn't change any control flow or existing logging.
|
|
407
366
|
const rebased = [];
|
|
408
367
|
const skippedDirty = [];
|
|
409
368
|
const skippedLocked = [];
|
|
@@ -416,11 +375,11 @@ export async function runRebaseCycle(ctx) {
|
|
|
416
375
|
return { rebased, skippedDirty, skippedLocked, failed };
|
|
417
376
|
}
|
|
418
377
|
for (const branchDir of branchDirs) {
|
|
419
|
-
// Known structural entries under content-branches/, not branch
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
378
|
+
// Known structural entries under content-branches/, not branch workspaces:
|
|
379
|
+
// branches.json (registry snapshot) and dot-prefixed entries
|
|
380
|
+
// (.canopy-meta/, transient lock dirs). Skipped SILENTLY so the no-.git
|
|
381
|
+
// skip logs below don't fire for them every cycle; everything else still
|
|
382
|
+
// logs loudly on skip.
|
|
424
383
|
if (branchDir.startsWith('.') || branchDir === 'branches.json') {
|
|
425
384
|
continue;
|
|
426
385
|
}
|
|
@@ -438,11 +397,11 @@ export async function runRebaseCycle(ctx) {
|
|
|
438
397
|
continue;
|
|
439
398
|
}
|
|
440
399
|
// The base branch's own clone is refreshed ff-only by
|
|
441
|
-
// refreshBaseBranchWorkspace() earlier in syncGit(). Routing it
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
400
|
+
// refreshBaseBranchWorkspace() earlier in syncGit(). Routing it through
|
|
401
|
+
// this conflict-resolution loop could rewrite its history (the --theirs
|
|
402
|
+
// loop below) and stamp meaningless conflict metadata on it. Compare
|
|
403
|
+
// sanitized-vs-sanitized: branchDir is a filesystem name, ctx.baseBranch
|
|
404
|
+
// is raw.
|
|
446
405
|
if (branchDir === ctx.sanitizedBaseBranch) {
|
|
447
406
|
workerLog(` Skipping ${branchDir}: base branch (refreshed separately)`);
|
|
448
407
|
continue;
|
|
@@ -477,19 +436,17 @@ export async function runRebaseCycle(ctx) {
|
|
|
477
436
|
* rest of the cycle ([SYNC-M2]).
|
|
478
437
|
*/
|
|
479
438
|
async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
480
|
-
//
|
|
481
|
-
//
|
|
439
|
+
// Read only by the catch below. See the `rebased` rider on
|
|
440
|
+
// BranchRebaseOutcome.
|
|
482
441
|
let didRebase = false;
|
|
483
442
|
try {
|
|
484
|
-
// Load metadata before any git ops to check branch status
|
|
485
443
|
const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
486
444
|
const branchStatus = metaFile?.branch.status;
|
|
487
|
-
// Skip branches that
|
|
488
|
-
// - submitted/approved: in review,
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// - archived: already merged,
|
|
492
|
-
// poll (avoid the wasted API call).
|
|
445
|
+
// Skip branches that must not be mutated:
|
|
446
|
+
// - submitted/approved: in review, so never rewrite history under an active
|
|
447
|
+
// PR -- but do poll GitHub for the PR's resolution, since nothing else
|
|
448
|
+
// tells the worker a merge/close happened;
|
|
449
|
+
// - archived: already merged, with no PR left to poll.
|
|
493
450
|
if (branchStatus === 'submitted' || branchStatus === 'approved') {
|
|
494
451
|
workerLog(` Skipping ${branchDir} (${branchStatus})`);
|
|
495
452
|
await pollMergeState(ctx, branchDir, branchPath, metaFile);
|
|
@@ -501,35 +458,31 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
501
458
|
}
|
|
502
459
|
const branchGit = simpleGit({
|
|
503
460
|
baseDir: branchPath,
|
|
504
|
-
// Keep git non-interactive during rebase/merge so it never blocks on an
|
|
505
|
-
// simple-git >=3.32
|
|
506
|
-
// value
|
|
507
|
-
// so
|
|
461
|
+
// Keep git non-interactive during rebase/merge so it never blocks on an
|
|
462
|
+
// editor. simple-git >=3.32 requires opting in to set core.editor; the
|
|
463
|
+
// value is a hardcoded literal ("true", the shell no-op), not user input,
|
|
464
|
+
// so allowUnsafeEditor carries no injection risk here.
|
|
508
465
|
config: ['core.editor=true'],
|
|
509
466
|
unsafe: { allowUnsafeEditor: true },
|
|
510
467
|
});
|
|
511
468
|
// [SYNC-C1] Take the branch's cross-host content-write lock BEFORE the
|
|
512
|
-
// dirty check, and hold it for the whole rebase.
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
// N conflict rounds of awaited git subprocesses on EFS -- a save is
|
|
519
|
-
// destroyed two ways: `checkout --theirs` below overwrites the
|
|
520
|
-
// just-saved file with the branch's committed version and the rebase
|
|
521
|
-
// then SUCCEEDS (nothing logs a failure at all), and `rebase --abort`
|
|
469
|
+
// dirty check, and hold it for the whole rebase. The dirty check alone is
|
|
470
|
+
// check-then-act, and its residual window spans fetch, replay and N
|
|
471
|
+
// conflict rounds of awaited git subprocesses on EFS. A save landing in
|
|
472
|
+
// there is destroyed two ways: `checkout --theirs` below overwrites the
|
|
473
|
+
// just-saved file with the branch's committed version and the rebase then
|
|
474
|
+
// SUCCEEDS (nothing logs a failure at all), and `rebase --abort`
|
|
522
475
|
// hard-resets the tree. The editor already got its 200 either way.
|
|
523
476
|
//
|
|
524
477
|
// Zero-retry acquisition, deliberately: on contention this branch is
|
|
525
|
-
// skipped and retried
|
|
526
|
-
//
|
|
527
|
-
//
|
|
478
|
+
// skipped and retried next sync cycle (~5 min), the same principle as the
|
|
479
|
+
// skip-dirty-branches behaviour below. Writers get the patient side of the
|
|
480
|
+
// asymmetry -- see utils/content-write-lock.ts.
|
|
528
481
|
//
|
|
529
|
-
// The heartbeat
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
482
|
+
// The heartbeat keeping this lock fresh (proper-lockfile refreshes every
|
|
483
|
+
// `stale`/2 = 15s) is a timer on this event loop, and every git step below
|
|
484
|
+
// is an awaited subprocess rather than a synchronous block, so the refresh
|
|
485
|
+
// keeps firing for the whole hold.
|
|
533
486
|
let releaseContentLock;
|
|
534
487
|
// [SYNC-C1] If the lock is lost mid-hold, a writer may now be live
|
|
535
488
|
// against this same tree. Every git step below is destructive, so
|
|
@@ -552,55 +505,47 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
552
505
|
throw lockErr;
|
|
553
506
|
}
|
|
554
507
|
try {
|
|
555
|
-
// Recover an INTERRUPTED rebase before anything else looks at this
|
|
556
|
-
//
|
|
557
|
-
// uncommitted changes, so without this the dirty check below
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// it is the only thing that ever rebases these clones, and it got
|
|
564
|
-
// here via a crash, an OOM, a spot interruption, or the ASG rolling
|
|
565
|
-
// the instance (which happens on EVERY `cdk deploy`, while `stop()`
|
|
566
|
-
// drains for at most taskTimeoutMs).
|
|
508
|
+
// Recover an INTERRUPTED rebase before anything else looks at this tree.
|
|
509
|
+
// A clone left with .git/rebase-merge (or rebase-apply) reports
|
|
510
|
+
// uncommitted changes, so without this the dirty check below classifies
|
|
511
|
+
// it `skippedDirty` on every cycle FOREVER while branch-health scans it
|
|
512
|
+
// as healthy -- and editors meanwhile read, and can save over,
|
|
513
|
+
// conflict-marker content. An in-progress rebase here is always this
|
|
514
|
+
// worker's own abandoned work (nothing else rebases these clones) left by
|
|
515
|
+
// a crash, an OOM, a spot interruption, or the ASG rolling the instance.
|
|
567
516
|
//
|
|
568
|
-
// NOT LOSSLESS, and it is important not to claim otherwise. `git
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
517
|
+
// NOT LOSSLESS, and it is important not to claim otherwise. `git rebase
|
|
518
|
+
// --abort` hard-resets tracked files to the pre-rebase head. While the
|
|
519
|
+
// worker was DOWN nothing held the [SYNC-C1] content-write lock, so an
|
|
520
|
+
// editor could have saved into this wedged clone and received a 200; that
|
|
521
|
+
// save is a working-tree modification and the abort reverts it (new,
|
|
522
|
+
// untracked entry files survive; edits to existing ones do not). Taking
|
|
523
|
+
// the lock here stops any FURTHER save racing the abort but cannot
|
|
524
|
+
// recover one that already landed.
|
|
576
525
|
//
|
|
577
|
-
// Aborting anyway is still
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
// first, which is the only record an operator would have.
|
|
526
|
+
// Aborting anyway is still right -- the alternative is a branch wedged
|
|
527
|
+
// forever serving conflict-marker content to editors -- but it must not
|
|
528
|
+
// happen SILENTLY, so anything modified beyond the rebase's own conflict
|
|
529
|
+
// state is logged by path first. That log is the operator's only record.
|
|
582
530
|
if (await isRebaseInProgress(branchPath)) {
|
|
583
531
|
const preAbort = await branchGit.status().catch(() => null);
|
|
584
|
-
// Keyed on the WORKING-TREE column
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
532
|
+
// Keyed on the WORKING-TREE column ONLY. The two porcelain columns mean
|
|
533
|
+
// different things here, and conflating them reports false data loss on
|
|
534
|
+
// essentially every conflict-wedged recovery (verified against real
|
|
535
|
+
// git, mid-rebase):
|
|
588
536
|
//
|
|
589
537
|
// `M ` index=M, wd=' ' -- the interrupted replay's own cleanly
|
|
590
|
-
// merged files, already STAGED
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// the worker was down. The abort
|
|
596
|
-
// discards exactly these.
|
|
538
|
+
// merged files, already STAGED: committed
|
|
539
|
+
// history that survives the abort untouched.
|
|
540
|
+
// ` M` index=' ', wd=M -- an unstaged working-tree modification: an
|
|
541
|
+
// editor's save landing while the worker was
|
|
542
|
+
// down. The abort discards exactly these.
|
|
597
543
|
// `??` -- untracked; the abort leaves them.
|
|
598
544
|
//
|
|
599
|
-
// KNOWN GAP
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
// the two apart. Those discards go unlogged.
|
|
545
|
+
// KNOWN GAP: a save onto one of the rebase's own conflicted paths (the
|
|
546
|
+
// "saved over conflict-marker content" case) is excluded below, because
|
|
547
|
+
// the file reads `UU` whether or not an editor touched it and status
|
|
548
|
+
// alone cannot tell the two apart. Those discards go unlogged.
|
|
604
549
|
const collateral = (preAbort?.files ?? [])
|
|
605
550
|
.filter((f) => !preAbort?.conflicted.includes(f.path))
|
|
606
551
|
.filter((f) => f.working_dir !== ' ' && f.working_dir !== '?')
|
|
@@ -619,37 +564,34 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
619
564
|
const reason = redactCredentials(`could not abort interrupted rebase: ${getErrorMessage(abortErr)}`);
|
|
620
565
|
workerLogWarn(` Skipping ${branchDir}: ${reason}`);
|
|
621
566
|
// Record on branch metadata too, like the other two failure exits
|
|
622
|
-
// (`!completed` and the outer catch)
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// since it is the one the next cycle cannot fix by itself.
|
|
567
|
+
// (`!completed` and the outer catch): without a `syncFailureReason`
|
|
568
|
+
// the admin branch panel shows nothing for a persistently
|
|
569
|
+
// un-abortable wedge, which is precisely the state needing an
|
|
570
|
+
// operator, since the next cycle cannot fix it by itself.
|
|
627
571
|
await recordRebaseFailure(ctx, branchPath, branchDir, reason);
|
|
628
572
|
return { kind: 'failed', error: reason };
|
|
629
573
|
}
|
|
630
574
|
}
|
|
631
|
-
// Skip dirty branches — editor has
|
|
632
|
-
//
|
|
633
|
-
//
|
|
575
|
+
// Skip dirty branches — the editor has changes that cannot be rebased.
|
|
576
|
+
// Inside the lock, so no write can land between this check and the rebase
|
|
577
|
+
// below.
|
|
634
578
|
const dirtyCheck = await branchGit.status();
|
|
635
579
|
if (dirtyCheck.files.length > 0) {
|
|
636
580
|
workerLog(` Skipping ${branchDir}: has uncommitted changes`);
|
|
637
581
|
return { kind: 'skippedDirty' };
|
|
638
582
|
}
|
|
639
|
-
// The clone's own ref name: branchDir is the sanitized DIRECTORY
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// which is the safe direction.
|
|
583
|
+
// The clone's own ref name: branchDir is the sanitized DIRECTORY name and
|
|
584
|
+
// need not match it. A literal 'HEAD' means a detached clone (a crashed
|
|
585
|
+
// rebase can leave one), where nothing below can safely name a ref, so
|
|
586
|
+
// every publish path stays disarmed -- the safe direction.
|
|
644
587
|
const branchRef = (await branchGit.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
|
645
588
|
const canPublish = branchRef.length > 0 && branchRef !== 'HEAD';
|
|
646
589
|
// [SYNC-H1] Self-heal: finish an interrupted publish even when this
|
|
647
|
-
// branch is not behind base. Every crash window in the arming
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
// of them, every cycle) cost nothing extra.
|
|
590
|
+
// branch is not behind base. Every crash window in the arming sequence
|
|
591
|
+
// below leaves the marker set with the work unfinished, and this is what
|
|
592
|
+
// completes it -- without it one lost lease race strands the branch until
|
|
593
|
+
// the base happens to advance again. Gated on the loop-top snapshot, so
|
|
594
|
+
// unmarked branches (nearly all of them, every cycle) cost nothing.
|
|
653
595
|
if (canPublish && metaFile?.branch.historyRewrittenFrom) {
|
|
654
596
|
await reconcilePendingRewrite(ctx, {
|
|
655
597
|
branchPath,
|
|
@@ -660,38 +602,34 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
660
602
|
});
|
|
661
603
|
}
|
|
662
604
|
await branchGit.fetch('origin', ctx.baseBranch);
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// shared mutable file per repo, repointed by any concurrent fetch.
|
|
605
|
+
// rev-list, not status.behind, which needs an upstream tracking branch
|
|
606
|
+
// that checkoutBranch's fallback paths do not always configure. Against
|
|
607
|
+
// the just-fetched tip, not origin/<base>: branch clones are
|
|
608
|
+
// --single-branch, so no remote-tracking ref exists for any base branch
|
|
609
|
+
// other than the one they were cloned from. Pinned to a SHA immediately
|
|
610
|
+
// — FETCH_HEAD is one shared mutable file per repo, repointed by any
|
|
611
|
+
// concurrent fetch.
|
|
671
612
|
const fetchedBaseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
|
|
672
613
|
const behindCount = parseInt((await branchGit.raw(['rev-list', '--count', `HEAD..${fetchedBaseTip}`])).trim(), 10);
|
|
673
614
|
const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
|
|
674
615
|
if (behindCount === 0) {
|
|
675
|
-
// Already in sync
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
// concurrent editor-driven metadata change during that window
|
|
685
|
-
// isn't clobbered by a stale skip decision.
|
|
616
|
+
// Already in sync -- the overwhelmingly common outcome per branch per
|
|
617
|
+
// cycle, so skip the save entirely when metadata already reflects a
|
|
618
|
+
// clean state. save() eager-regenerates the branch registry
|
|
619
|
+
// (branch-metadata.ts's invalidateRegistry(), O(branch count) EFS
|
|
620
|
+
// reads), so an unconditional save here makes every rebase cycle
|
|
621
|
+
// O(N^2) registry work for a true no-op. Re-load fresh, not the
|
|
622
|
+
// `metaFile` snapshot from before the fetch/rev-list above, so a
|
|
623
|
+
// concurrent editor-driven metadata change in that window is not
|
|
624
|
+
// clobbered by a stale skip decision.
|
|
686
625
|
const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
687
626
|
const conflictStatus = currentMeta?.branch.conflictStatus;
|
|
688
627
|
const conflictFiles = currentMeta?.branch.conflictFiles;
|
|
689
628
|
const conflictAlreadyClean = (conflictStatus === undefined || conflictStatus === 'clean') &&
|
|
690
629
|
(conflictFiles === undefined || conflictFiles.length === 0);
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
// caught up, so no other save site would ever clear it).
|
|
630
|
+
// A lingering rebaseFailure must also be cleared once the branch
|
|
631
|
+
// catches up clean, or it sticks as a stale warning forever: nothing
|
|
632
|
+
// else touches a caught-up branch, so no other save site clears it.
|
|
695
633
|
const alreadyClean = conflictAlreadyClean && currentMeta?.branch.rebaseFailure === undefined;
|
|
696
634
|
if (alreadyClean) {
|
|
697
635
|
return { kind: 'none' };
|
|
@@ -707,10 +645,10 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
707
645
|
return { kind: 'none' };
|
|
708
646
|
}
|
|
709
647
|
workerLog(`Rebasing ${branchDir} (${behindCount} commits behind)...`);
|
|
710
|
-
// Read BOTH sides before rewriting anything: the arming guard after
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
648
|
+
// Read BOTH sides before rewriting anything: the arming guard after the
|
|
649
|
+
// rebase compares what remote.git published against what this clone is
|
|
650
|
+
// about to rebase away, and the clone's pre-rebase tip is exactly what
|
|
651
|
+
// the rebase makes unreadable.
|
|
714
652
|
const preRebaseHead = (await branchGit.revparse(['HEAD'])).trim();
|
|
715
653
|
const publishedSha = canPublish ? await readPublishedSha(ctx, branchRef) : null;
|
|
716
654
|
const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, () => contentLockCompromised);
|
|
@@ -719,27 +657,25 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
719
657
|
if (completed)
|
|
720
658
|
await ctx.afterRebaseCompletedForTesting();
|
|
721
659
|
// [SYNC-C1] A lost lock is a RETRY, not a rebase failure: nothing is
|
|
722
|
-
// wrong with the branch, we simply can no longer prove we were the
|
|
723
|
-
//
|
|
724
|
-
//
|
|
660
|
+
// wrong with the branch, we simply can no longer prove we were the only
|
|
661
|
+
// writer. Handled ahead of the `!completed` block so it never records a
|
|
662
|
+
// user-visible rebaseFailure or lands in `failed[]`.
|
|
725
663
|
//
|
|
726
664
|
// ONLY when the rebase did not complete. Bailing out of a COMPLETED
|
|
727
|
-
// rebase
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
// wedged published branch.
|
|
665
|
+
// rebase is worse than useless: the history is already rewritten (so
|
|
666
|
+
// `--abort` is a no-op), and skipping the completion path below strands
|
|
667
|
+
// three things the next cycle never redoes, because a caught-up branch
|
|
668
|
+
// short-circuits at the `behindCount === 0` check above -- the [SYNC-H1]
|
|
669
|
+
// `markHistoryRewritten` marker (without which the editor's next submit
|
|
670
|
+
// is rejected non-fast-forward and mis-diagnosed as another deployment),
|
|
671
|
+
// the content-cache invalidation for a tree that DID change, and the
|
|
672
|
+
// conflictStatus save. That turns a transient lock compromise into a
|
|
673
|
+
// permanently wedged published branch.
|
|
737
674
|
//
|
|
738
675
|
// Nor does bailing protect a racing save: a rebase replays COMMITTED
|
|
739
|
-
// history
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// lost exclusivity loudly and finish the job.
|
|
676
|
+
// history while a concurrent save is uncommitted working-tree state, and
|
|
677
|
+
// that writer is already told to retry via ContentWriteLockBusyError. So
|
|
678
|
+
// when the rebase completed, log the lost exclusivity loudly and finish.
|
|
743
679
|
if (contentLockCompromised && !completed) {
|
|
744
680
|
workerLogWarn(` Skipping ${branchDir}: content-write lock was compromised mid-rebase (retrying next cycle)`);
|
|
745
681
|
// No-op when no rebase is in progress; failure is expected there.
|
|
@@ -750,32 +686,30 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
750
686
|
workerLogWarn(` Content-write lock for ${branchDir} was compromised, but its rebase had already completed -- finishing the sync (history is rewritten; skipping now would strand the history-rewrite marker and wedge the branch)`);
|
|
751
687
|
}
|
|
752
688
|
if (!completed) {
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
// not conflate the two.
|
|
689
|
+
// failureReason is only set on the "unexpected error" break above;
|
|
690
|
+
// MAX_REBASE_ROUNDS exhaustion is a distinct exit with no message of
|
|
691
|
+
// its own, so the warn text must not conflate the two.
|
|
757
692
|
workerLogWarn(failureReason !== undefined
|
|
758
693
|
? ` Rebase of ${branchDir} aborted due to unexpected error: ${failureReason}`
|
|
759
694
|
: ` Rebase of ${branchDir} did not complete within ${MAX_REBASE_ROUNDS} rounds, aborting`);
|
|
760
695
|
await branchGit.rebase(['--abort']).catch(() => { });
|
|
761
696
|
const rebaseFailureMessage = failureReason ?? `did not complete within ${MAX_REBASE_ROUNDS} rounds`;
|
|
762
|
-
// [REDACT] failed[] folds into worker-status.json's
|
|
763
|
-
//
|
|
764
|
-
//
|
|
697
|
+
// [REDACT] failed[] folds into worker-status.json's lastGitSync.failed,
|
|
698
|
+
// served to the browser -- failureReason can be an arbitrary git error
|
|
699
|
+
// message that embeds the bot token.
|
|
765
700
|
const redactedRebaseFailureMessage = redactCredentials(rebaseFailureMessage);
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
// outside this round loop, e.g. fetch/rev-list failures).
|
|
701
|
+
// Record ONCE here for the "!completed" exit: the unexpected-error
|
|
702
|
+
// break above always falls through to this block, so recording at the
|
|
703
|
+
// break itself would double-record. The outer catch below is the only
|
|
704
|
+
// other record site, covering a disjoint failure class -- errors
|
|
705
|
+
// outside the round loop, e.g. fetch/rev-list failures.
|
|
772
706
|
await recordRebaseFailure(ctx, branchPath, branchDir, redactedRebaseFailureMessage);
|
|
773
707
|
return { kind: 'failed', error: redactedRebaseFailureMessage };
|
|
774
708
|
}
|
|
775
|
-
// The rebase rewrote the
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
709
|
+
// The rebase rewrote the clone's working tree — mark ContentStore ID
|
|
710
|
+
// indexes rooted here stale so lookups rebuild from disk, in this process
|
|
711
|
+
// and (via the on-disk generation marker) in the Lambda containers
|
|
712
|
+
// sharing this filesystem.
|
|
779
713
|
await invalidateBranchContentCaches(branchPath);
|
|
780
714
|
const conflictIdsDeduped = conflictFilesToContentIds(conflictedFiles, ctx.contentRoot);
|
|
781
715
|
const hadConflicts = conflictIdsDeduped.length > 0;
|
|
@@ -787,15 +721,11 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
787
721
|
name: branchDir,
|
|
788
722
|
conflictStatus: hadConflicts ? 'conflicts-detected' : 'clean',
|
|
789
723
|
conflictFiles: conflictIdsDeduped,
|
|
790
|
-
//
|
|
791
|
-
//
|
|
724
|
+
// The cycle completed, so clear any prior failure record regardless
|
|
725
|
+
// of the conflict outcome.
|
|
792
726
|
rebaseFailure: undefined,
|
|
793
727
|
},
|
|
794
728
|
});
|
|
795
|
-
// PR-W1: the branch was behind and the rebase completed (with or
|
|
796
|
-
// without --theirs conflict resolution) -- it moved, so it belongs
|
|
797
|
-
// in the summary. Branches already up to date `continue`d above and
|
|
798
|
-
// are deliberately not listed here.
|
|
799
729
|
didRebase = true;
|
|
800
730
|
await carryForwardRewrittenHistory(ctx, {
|
|
801
731
|
branchPath,
|
|
@@ -807,24 +737,21 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
807
737
|
return { kind: 'rebased' };
|
|
808
738
|
}
|
|
809
739
|
finally {
|
|
810
|
-
// Last-resort guarantee that NO exit path leaves this clone
|
|
811
|
-
//
|
|
812
|
-
// above, which lands in the outer catch and previously only logged.
|
|
740
|
+
// Last-resort guarantee that NO exit path leaves this clone mid-rebase,
|
|
741
|
+
// including an unexpected throw from any git step above.
|
|
813
742
|
//
|
|
814
|
-
// It must live HERE rather than in
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
// abort
|
|
822
|
-
//
|
|
823
|
-
// worse outcome, and the writer in that window is already being told
|
|
824
|
-
// to retry.
|
|
743
|
+
// It must live HERE rather than in the outer catch: that catch runs AFTER
|
|
744
|
+
// this finally has released the content-write lock, so aborting there
|
|
745
|
+
// would hard-reset a working tree an editor's save could already be
|
|
746
|
+
// racing -- precisely the [SYNC-C1] hazard the lock exists to prevent.
|
|
747
|
+
// Inside the finally the lock is still held, EXCEPT on the narrow path
|
|
748
|
+
// where it was compromised mid-hold, where a newly-admitted writer may be
|
|
749
|
+
// live and this abort carries the same exposure as the compromise path's
|
|
750
|
+
// own abort above. Not special-cased: leaving a clone wedged mid-rebase
|
|
751
|
+
// is worse, and that writer is already being told to retry.
|
|
825
752
|
//
|
|
826
|
-
// Guarded on actual rebase state so the happy path and the
|
|
827
|
-
//
|
|
753
|
+
// Guarded on actual rebase state, so the happy path and the early returns
|
|
754
|
+
// cost one stat and do nothing.
|
|
828
755
|
try {
|
|
829
756
|
if (await isRebaseInProgress(branchPath)) {
|
|
830
757
|
workerLogWarn(` ${branchDir}: rebase still in progress on exit -- aborting so the clone is not left wedged`);
|
|
@@ -835,9 +762,9 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
835
762
|
// Best effort: the next cycle's recovery check retries this.
|
|
836
763
|
workerLogWarn(` Failed to abort in-progress rebase for ${branchDir}: ${getErrorMessage(abortErr)}`);
|
|
837
764
|
}
|
|
838
|
-
// [SYNC-C1] Released on EVERY exit -- the
|
|
839
|
-
// into the outer catch, and the happy path alike. A stranded lock
|
|
840
|
-
//
|
|
765
|
+
// [SYNC-C1] Released on EVERY exit -- the early returns above, a throw
|
|
766
|
+
// into the outer catch, and the happy path alike. A stranded lock would
|
|
767
|
+
// wedge every write to this branch until it went stale.
|
|
841
768
|
await releaseContentLock?.().catch((releaseErr) => {
|
|
842
769
|
workerLogWarn(` Failed to release content-write lock for ${branchDir}: ${getErrorMessage(releaseErr)}`);
|
|
843
770
|
});
|
|
@@ -846,13 +773,11 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
846
773
|
catch (err) {
|
|
847
774
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
848
775
|
workerLogWarn(` Failed to sync ${branchDir}: ${message}`);
|
|
849
|
-
// [REDACT] Same rationale as the `if (!completed)`
|
|
850
|
-
//
|
|
851
|
-
// embed the bot token.
|
|
776
|
+
// [REDACT] Same rationale as the `if (!completed)` site above: this catches
|
|
777
|
+
// fetch/rev-list/unexpected errors, whose message can embed the bot token.
|
|
852
778
|
const redactedMessage = redactCredentials(message);
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
// disjoint.
|
|
779
|
+
// Second and only other record site -- see the `if (!completed)` block
|
|
780
|
+
// above for why the two are disjoint.
|
|
856
781
|
await recordRebaseFailure(ctx, branchPath, branchDir, redactedMessage);
|
|
857
782
|
return { kind: 'failed', error: redactedMessage, rebased: didRebase };
|
|
858
783
|
}
|