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
|
@@ -2,46 +2,41 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { simpleGit } from 'simple-git';
|
|
4
4
|
import lockfile from 'proper-lockfile';
|
|
5
|
-
import { recoverOrphanedTasks, cmsTaskQueueLogger } from '
|
|
5
|
+
import { recoverOrphanedTasks, cmsTaskQueueLogger } from '../task-queue/cms-task-queue.js';
|
|
6
6
|
import { createCanopyOctokit } from '../github-service.js';
|
|
7
7
|
import { isTransientAuthFailure, resolveWorkerGitHubAuth, } from './github-auth.js';
|
|
8
8
|
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/branch-name.js';
|
|
9
9
|
import { resolveDeploymentName } from '../operating-mode/deployment-name.js';
|
|
10
10
|
import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
|
|
11
|
-
import { writeWorkerStatus } from '
|
|
11
|
+
import { writeWorkerStatus } from '../task-queue/worker-status.js';
|
|
12
12
|
import { workerLog, workerLogWarn, workerLogError } from './log.js';
|
|
13
13
|
import { executeTask, orphanRecoveryMaxAgeMs, processTaskQueue, pushBranchToGitHub, updateBranchMetadata, } from './task-runner.js';
|
|
14
14
|
import { pollMergeState, runRebaseCycle } from './rebase.js';
|
|
15
15
|
import { cleanupTrashedBranchDirs, pushSettingsBranches, refreshBaseBranchWorkspace, syncGit, } from './git-sync.js';
|
|
16
16
|
// Re-exported because this module is the package's advertised worker
|
|
17
|
-
// entrypoint (`canopycms/worker/cms-worker`)
|
|
18
|
-
// before the task-queue cluster moved to ./task-runner. cms-worker.test.ts
|
|
19
|
-
// imports them from this path.
|
|
17
|
+
// entrypoint (`canopycms/worker/cms-worker`).
|
|
20
18
|
export { PermanentTaskError, isPermanentTaskFailure } from './task-runner.js';
|
|
21
19
|
// Re-exported so the AWS entrypoint (packages/canopycms-cdk/worker/index.ts)
|
|
22
|
-
// can prefix its own startup lines through the same helpers without
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// the previous CloudWatch event; see ./log.ts.
|
|
20
|
+
// can prefix its own startup lines through the same helpers without a new
|
|
21
|
+
// package entrypoint. Every line in worker.log must carry the timestamp prefix
|
|
22
|
+
// or it is folded into the previous CloudWatch event; see ./log.ts.
|
|
26
23
|
export { workerLog, workerLogWarn, workerLogError, installWorkerLogger } from './log.js';
|
|
27
|
-
// Re-exported for the same reason: an entrypoint that authenticates as a
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
24
|
+
// Re-exported for the same reason: an entrypoint that authenticates as a GitHub
|
|
25
|
+
// App builds the credential itself (core must not import `@octokit/auth-app` —
|
|
26
|
+
// see github-auth.ts) and needs the shape to inject and the key normalizer to
|
|
27
|
+
// apply.
|
|
31
28
|
export { normalizeGitHubAppPrivateKey, DEFAULT_GIT_TOKEN_MINT_TIMEOUT_MS, DEFAULT_GITHUB_TOKEN_REFRESH_MIN_INTERVAL_MS, } from './github-auth.js';
|
|
32
29
|
const DEFAULT_TASK_TIMEOUT = 60_000;
|
|
33
30
|
const DEFAULT_MAX_RETRIES = 3;
|
|
34
31
|
const DEFAULT_LOCK_STALE_MS = 60_000;
|
|
35
32
|
/**
|
|
36
|
-
* CMS Worker daemon
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* - Rebasing active branch workspaces
|
|
41
|
-
* - Refreshing auth metadata cache (via pluggable callback)
|
|
33
|
+
* CMS Worker daemon: the operations Lambda, which has no internet, cannot
|
|
34
|
+
* perform -- draining the task queue, syncing the bare repo with GitHub,
|
|
35
|
+
* rebasing branch workspaces, and refreshing the auth metadata cache through a
|
|
36
|
+
* pluggable callback.
|
|
42
37
|
*
|
|
43
|
-
* Auth-agnostic
|
|
44
|
-
*
|
|
38
|
+
* Auth-agnostic (no specific auth provider) and cloud-agnostic (git/Octokit
|
|
39
|
+
* directly, no AWS SDK dependency).
|
|
45
40
|
*/
|
|
46
41
|
export class CmsWorker {
|
|
47
42
|
constructor(config) {
|
|
@@ -64,12 +59,10 @@ export class CmsWorker {
|
|
|
64
59
|
this.contentRoot = config.contentRoot ?? 'content';
|
|
65
60
|
}
|
|
66
61
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* start()
|
|
70
|
-
*
|
|
71
|
-
* (defensive -- see start()'s catch), and (2) unit tests that exercise
|
|
72
|
-
* syncGit()/processTaskQueue() directly without calling start() first.
|
|
62
|
+
* This worker's self-reported status object, initialized on first use.
|
|
63
|
+
* Normally set at the top of start(); the lazy fallback covers something in
|
|
64
|
+
* start() reaching a status-write point before that (see start()'s catch) and
|
|
65
|
+
* unit tests driving syncGit()/processTaskQueue() without calling start().
|
|
73
66
|
*/
|
|
74
67
|
ensureStatusReport() {
|
|
75
68
|
if (!this.statusReport) {
|
|
@@ -82,28 +75,23 @@ export class CmsWorker {
|
|
|
82
75
|
* Resolve this deployment's settings branch, throwing if the infra-stamped
|
|
83
76
|
* deployment name is not a valid git ref component.
|
|
84
77
|
*
|
|
85
|
-
* Routed through the shared resolver, not a local `?? 'prod'`:
|
|
78
|
+
* Routed through the shared resolver, not a local `?? 'prod'`: that is the
|
|
86
79
|
* single definition of the env > config > mode-default precedence the Lambda
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* DEFERRED out of the constructor deliberately,
|
|
93
|
-
*
|
|
94
|
-
* CmsWorker(...)`
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* worker-status.json and surfaces in the admin panel.
|
|
103
|
-
*
|
|
104
|
-
* Lazy rather than start()-only so the value is still available to unit tests
|
|
105
|
-
* that drive pushSettingsBranches() directly without calling start() -- the
|
|
106
|
-
* same reason ensureStatusReport() above is shaped this way. Idempotent: the
|
|
80
|
+
* follows, and the only place the resolved value is validated. Without it the
|
|
81
|
+
* worker can silently own a different settings branch than the Lambda writing
|
|
82
|
+
* to the same workspace, and pushSettingsBranches then reports the real branch
|
|
83
|
+
* as foreign and never pushes it.
|
|
84
|
+
*
|
|
85
|
+
* DEFERRED out of the constructor deliberately, which is the whole point of
|
|
86
|
+
* the method existing: canopycms-cdk/worker/index.ts constructs the worker
|
|
87
|
+
* before calling start(), so a throw during `new CmsWorker(...)` lands BEFORE
|
|
88
|
+
* the only code that writes `lastFatalError` -- start()'s catch. Under
|
|
89
|
+
* systemd `Type=simple` + `Restart=always` with no cfn-signal, that is an
|
|
90
|
+
* invisible ~5s crash-loop that `cdk deploy` reports as success while the
|
|
91
|
+
* admin panel shows the worker 'absent' with no fatal error to explain it.
|
|
92
|
+
*
|
|
93
|
+
* Lazy rather than start()-only so unit tests driving pushSettingsBranches()
|
|
94
|
+
* still see the value, exactly as ensureStatusReport() above. Idempotent: the
|
|
107
95
|
* resolver is pure, so a later call returns the identical string.
|
|
108
96
|
*/
|
|
109
97
|
ensureSettingsBranch() {
|
|
@@ -118,15 +106,11 @@ export class CmsWorker {
|
|
|
118
106
|
* Build the {@link WorkerContext} handed to the extracted clusters
|
|
119
107
|
* (task-runner.ts, git-sync.ts, rebase.ts, history-rewrite.ts).
|
|
120
108
|
*
|
|
121
|
-
* Built FRESH on every call
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* hooks. A context that captured any of those at construction time would hand
|
|
127
|
-
* the extracted code the pre-test value -- which for `buildGitHubUrl` means a
|
|
128
|
-
* test's push going to github.com for real instead of its local fixture repo.
|
|
129
|
-
* See WorkerContext's doc comment for the full list.
|
|
109
|
+
* Built FRESH on every call, with instance-backed members as functions rather
|
|
110
|
+
* than copied values. See WorkerContext's INVARIANT: a context that captured
|
|
111
|
+
* any of them at construction time would hand the extracted code the pre-test
|
|
112
|
+
* value, which for `buildGitHubUrl` means a test's push going to github.com
|
|
113
|
+
* for real instead of its local fixture repo.
|
|
130
114
|
*/
|
|
131
115
|
ctx() {
|
|
132
116
|
return {
|
|
@@ -159,49 +143,37 @@ export class CmsWorker {
|
|
|
159
143
|
this.running = true;
|
|
160
144
|
workerLog('CMS Worker starting...');
|
|
161
145
|
this.ensureStatusReport();
|
|
162
|
-
// Acquire lock to prevent concurrent workers
|
|
163
146
|
await this.acquireLock();
|
|
164
|
-
// Everything below runs while holding the cross-host worker lock. A
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// rethrowing so the next start() (this process's retry, or another host)
|
|
170
|
-
// can acquire immediately. We deliberately do NOT reorder ensureRemoteGit
|
|
147
|
+
// Everything below runs while holding the cross-host worker lock. A failure
|
|
148
|
+
// here (most notably the empty-remote guard inside ensureRemoteGit) means
|
|
149
|
+
// the process is about to exit and systemd (Restart=always) will retry —
|
|
150
|
+
// but a still-held lock would make every retry fail with ELOCKED for up to
|
|
151
|
+
// lockStaleMs, so release before rethrowing. Do NOT reorder ensureRemoteGit
|
|
171
152
|
// ahead of acquireLock: two hosts cold-starting at once would then both
|
|
172
|
-
// race to `git clone --bare` into the same remoteGitPath
|
|
173
|
-
//
|
|
174
|
-
// clones elsewhere) — acquiring the lock first is what already
|
|
175
|
-
// serializes that.
|
|
153
|
+
// race to `git clone --bare` into the same remoteGitPath, and acquiring the
|
|
154
|
+
// lock first is what serializes that.
|
|
176
155
|
try {
|
|
177
156
|
// FIRST inside the try, before any I/O: an infra-stamped deployment name
|
|
178
|
-
// that is not a valid git ref component throws
|
|
179
|
-
// below records it to worker-status.json.
|
|
180
|
-
// constructor (as this used to) put the throw outside every
|
|
181
|
-
// status-writing path -- see ensureSettingsBranch()'s doc comment.
|
|
157
|
+
// that is not a valid git ref component throws HERE, where the catch
|
|
158
|
+
// below records it to worker-status.json. See ensureSettingsBranch().
|
|
182
159
|
this.ensureSettingsBranch();
|
|
183
|
-
// Same
|
|
184
|
-
//
|
|
185
|
-
//
|
|
160
|
+
// Same shape, same reason: a half-configured credential throws HERE,
|
|
161
|
+
// inside the try, rather than out of `new CmsWorker(...)` where nothing
|
|
162
|
+
// could record it.
|
|
186
163
|
this.ensureGitHubAuth();
|
|
187
164
|
// BEFORE ensureRemoteGit(): its clone is the first thing to use the
|
|
188
165
|
// credential, and its catch blames the repository rather than the
|
|
189
166
|
// credential. See preflightGitHubAppAuth().
|
|
190
167
|
await this.preflightGitHubAppAuth();
|
|
191
|
-
// Ensure remote.git exists (init bare repo if first run)
|
|
192
168
|
await this.ensureRemoteGit();
|
|
193
|
-
// Recover
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// doc comment for why a boot-only call is insufficient once the
|
|
198
|
-
// worker's ASG rolls on every `cdk deploy` (CanopyCmsService's
|
|
199
|
-
// UpdatePolicy).
|
|
169
|
+
// Recover orphaned tasks immediately rather than waiting for the first
|
|
170
|
+
// processTaskQueue() poll. Not the only call site: processTaskQueue()
|
|
171
|
+
// repeats this every cycle, and its doc comment says why a boot-only call
|
|
172
|
+
// is insufficient.
|
|
200
173
|
const recovered = await recoverOrphanedTasks(this.taskDir, orphanRecoveryMaxAgeMs(this.ctx()), this.log);
|
|
201
174
|
if (recovered > 0) {
|
|
202
175
|
workerLog(`Recovered ${recovered} orphaned task(s)`);
|
|
203
176
|
}
|
|
204
|
-
// Run initial sync + cache refresh immediately
|
|
205
177
|
const initialTasks = [this.syncGit()];
|
|
206
178
|
if (this.config.refreshAuthCache) {
|
|
207
179
|
initialTasks.push(this.refreshAuthCache());
|
|
@@ -209,15 +181,15 @@ export class CmsWorker {
|
|
|
209
181
|
await Promise.allSettled(initialTasks);
|
|
210
182
|
}
|
|
211
183
|
catch (err) {
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
184
|
+
// Surface a startup failure (e.g. the empty-remote guard's poisoned
|
|
185
|
+
// remote.git) to the admin panel via worker-status.json, not only
|
|
186
|
+
// journald/CloudWatch. Best-effort and BEFORE releaseLock(): a
|
|
187
|
+
// status-write failure must never block releasing the lock.
|
|
216
188
|
const report = this.ensureStatusReport();
|
|
217
189
|
report.lastFatalError = {
|
|
218
|
-
// [REDACT] Persisted to worker-status.json and served to the
|
|
219
|
-
//
|
|
220
|
-
//
|
|
190
|
+
// [REDACT] Persisted to worker-status.json and served to the browser by
|
|
191
|
+
// the admin panel -- must never carry the bot token a poisoned or
|
|
192
|
+
// failed git URL (buildGitHubUrl()) can embed.
|
|
221
193
|
message: redactCredentials(getErrorMessage(err)),
|
|
222
194
|
at: new Date().toISOString(),
|
|
223
195
|
phase: 'startup',
|
|
@@ -231,15 +203,13 @@ export class CmsWorker {
|
|
|
231
203
|
await this.releaseLock();
|
|
232
204
|
throw err;
|
|
233
205
|
}
|
|
234
|
-
// Start recurring task loops using setTimeout chaining
|
|
235
|
-
// (avoids setInterval overlap when tasks take longer than the interval)
|
|
236
206
|
const taskInterval = this.config.taskPollInterval ?? 5_000;
|
|
237
207
|
const gitInterval = this.config.gitSyncInterval ?? 5 * 60_000;
|
|
238
208
|
this.scheduleLoop(() => this.processTaskQueue(), taskInterval);
|
|
239
209
|
// The wrapper, not syncGit() itself: a failed sync is where a rotated or
|
|
240
|
-
// revoked GitHub credential is noticed.
|
|
241
|
-
//
|
|
242
|
-
//
|
|
210
|
+
// revoked GitHub credential is noticed. start()'s own initial syncGit()
|
|
211
|
+
// above stays unwrapped -- there is no stale credential to refresh one line
|
|
212
|
+
// after reading it at boot.
|
|
243
213
|
this.scheduleLoop(() => this.syncGitWithCredentialRefresh(), gitInterval);
|
|
244
214
|
if (this.config.refreshAuthCache) {
|
|
245
215
|
const cacheInterval = this.config.authCacheRefreshInterval ?? 15 * 60_000;
|
|
@@ -256,7 +226,6 @@ export class CmsWorker {
|
|
|
256
226
|
clearTimeout(t);
|
|
257
227
|
}
|
|
258
228
|
this.activeTimeouts.clear();
|
|
259
|
-
// Wait for all in-flight operations to complete (up to taskTimeoutMs)
|
|
260
229
|
let drainTimer;
|
|
261
230
|
await Promise.race([
|
|
262
231
|
Promise.allSettled([...this.activeOperations]),
|
|
@@ -272,26 +241,23 @@ export class CmsWorker {
|
|
|
272
241
|
* Acquire the cross-host worker lock (DEP-C2).
|
|
273
242
|
*
|
|
274
243
|
* The task queue is single-consumer (see task-queue/task-queue.ts): two
|
|
275
|
-
* concurrent workers would double-process tasks
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* and must never participate in staleness decisions.
|
|
244
|
+
* concurrent workers would double-process tasks, duplicating pushes and PRs.
|
|
245
|
+
* The workspace lives on EFS, so mutual exclusion must be sound ACROSS HOSTS
|
|
246
|
+
* — a PID liveness probe (`process.kill(pid, 0)`) means something only on the
|
|
247
|
+
* holder's own machine and must NEVER participate in staleness decisions.
|
|
280
248
|
*
|
|
281
249
|
* proper-lockfile provides a heartbeat lease with no PID involved: the lock
|
|
282
250
|
* is a directory created atomically (mkdir — atomic on NFS/EFS), the holder
|
|
283
|
-
* refreshes its mtime every lockStaleMs/2, and the lock is
|
|
284
|
-
*
|
|
285
|
-
* lockStaleMs. Liveness is judged purely by heartbeat freshness.
|
|
251
|
+
* refreshes its mtime every lockStaleMs/2, and the lock is abandoned, and
|
|
252
|
+
* taken over, only once that heartbeat is older than lockStaleMs.
|
|
286
253
|
*
|
|
287
254
|
* No acquire retries: a second worker exits immediately, matching daemon
|
|
288
|
-
* semantics
|
|
289
|
-
*
|
|
255
|
+
* semantics. After a crash the dead holder's heartbeat expires within
|
|
256
|
+
* lockStaleMs and the next start succeeds.
|
|
290
257
|
*
|
|
291
|
-
* Staleness
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* negligible, but a host with a badly wrong clock could misjudge liveness.
|
|
258
|
+
* Staleness compares the lock's mtime against the LOCAL clock, so correct
|
|
259
|
+
* cross-host takeover assumes reasonable clock agreement (NTP). Ordinary skew
|
|
260
|
+
* is negligible at the default TTL; a badly wrong clock misjudges liveness.
|
|
295
261
|
*/
|
|
296
262
|
async acquireLock() {
|
|
297
263
|
await fs.mkdir(this.taskDir, { recursive: true });
|
|
@@ -300,9 +266,9 @@ export class CmsWorker {
|
|
|
300
266
|
lockfilePath: this.lockFilePath,
|
|
301
267
|
stale: this.lockStaleMs,
|
|
302
268
|
onCompromised: (err) => {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
269
|
+
// The heartbeat could not be maintained (lock deleted or taken over),
|
|
270
|
+
// so another worker may now be consuming the queue: stop processing
|
|
271
|
+
// to preserve the single-consumer invariant.
|
|
306
272
|
workerLogError('Worker lock compromised, shutting down:', getErrorMessage(err));
|
|
307
273
|
this.releaseLockFn = null; // the lock is already lost; nothing to release
|
|
308
274
|
void this.stop();
|
|
@@ -329,9 +295,9 @@ export class CmsWorker {
|
|
|
329
295
|
}
|
|
330
296
|
}
|
|
331
297
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
298
|
+
* Run `fn` repeatedly, the next invocation starting `interval` ms after the
|
|
299
|
+
* previous one COMPLETES. setTimeout chaining rather than setInterval, so
|
|
300
|
+
* executions cannot overlap when one runs longer than the interval.
|
|
335
301
|
*/
|
|
336
302
|
scheduleLoop(fn, interval) {
|
|
337
303
|
const run = () => {
|
|
@@ -354,18 +320,16 @@ export class CmsWorker {
|
|
|
354
320
|
/**
|
|
355
321
|
* Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
|
|
356
322
|
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* GitManager.bareRemoteHasBranch for the same pattern).
|
|
323
|
+
* Explicit `--git-dir` rather than `simpleGit({ baseDir })`, so this also
|
|
324
|
+
* works where `safe.bareRepository=explicit` refuses cwd-based discovery of
|
|
325
|
+
* bare repos but expressly allows `--git-dir` (same pattern as
|
|
326
|
+
* GitManager.bareRemoteHasBranch).
|
|
362
327
|
*
|
|
363
|
-
* Deliberately omits `--quiet`: simple-git
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* is what makes simple-git reject the promise here.
|
|
328
|
+
* Deliberately omits `--quiet`: simple-git treats a task as failed only when
|
|
329
|
+
* the process exits non-zero AND writes to stderr, so a silent-on-failure
|
|
330
|
+
* `--verify` would leave a missing branch indistinguishable from success.
|
|
331
|
+
* Without `--quiet`, `rev-parse --verify` writes "fatal: ..." to stderr,
|
|
332
|
+
* which is what makes simple-git reject the promise here.
|
|
369
333
|
*/
|
|
370
334
|
async verifyBaseBranchExists(gitDir) {
|
|
371
335
|
await simpleGit().raw([
|
|
@@ -382,32 +346,27 @@ export class CmsWorker {
|
|
|
382
346
|
*
|
|
383
347
|
* `git clone https://x-access-token:<token>@github.com/...` records that URL
|
|
384
348
|
* verbatim as `remote.origin.url`, and for `remote.git` that config lives on
|
|
385
|
-
* shared EFS. The security model in docs/deploying-to-aws.md --
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* content the worker then pushes to GitHub.
|
|
349
|
+
* shared EFS. The security model in docs/deploying-to-aws.md -- a compromised
|
|
350
|
+
* Lambda can read/write EFS content but cannot push to GitHub, secrets stay
|
|
351
|
+
* on the worker -- is false while that string is there: the Lambda could read
|
|
352
|
+
* the token off EFS and, with no egress of its own, exfiltrate it by writing
|
|
353
|
+
* it into branch content the worker then pushes to GitHub.
|
|
391
354
|
*
|
|
392
355
|
* Nothing needs the remote: every push passes the URL explicitly as an
|
|
393
|
-
* argument
|
|
394
|
-
* `verifyBaseBranchExists` reads local refs.
|
|
356
|
+
* argument, and `verifyBaseBranchExists` reads local refs.
|
|
395
357
|
*
|
|
396
358
|
* VERIFIES rather than assuming: it re-reads the config and throws if the URL
|
|
397
|
-
* survives,
|
|
398
|
-
* scrub was indistinguishable from a successful one.
|
|
359
|
+
* survives, so a failed scrub is never indistinguishable from a clean one.
|
|
399
360
|
*/
|
|
400
361
|
async scrubPersistedRemote(gitDir) {
|
|
401
362
|
const git = simpleGit({ baseDir: gitDir });
|
|
402
|
-
// `git config --get` exits 1 with no output when the key is absent
|
|
403
|
-
// simple-git
|
|
404
|
-
//
|
|
405
|
-
// must be read as "absent" too. Treating "" as a surviving URL is what
|
|
406
|
-
// made the first version of this reject every clean scrub.
|
|
363
|
+
// `git config --get` exits 1 with no output when the key is absent, and
|
|
364
|
+
// simple-git resolves with an empty string rather than throwing (verified
|
|
365
|
+
// against 3.36), so an empty result means "absent" too.
|
|
407
366
|
//
|
|
408
|
-
// 'unreadable' is deliberately
|
|
409
|
-
// for any
|
|
410
|
-
//
|
|
367
|
+
// 'unreadable' is deliberately DISTINCT from 'absent'. A read that fails
|
|
368
|
+
// for any other reason must not be mistaken for "no token here": that would
|
|
369
|
+
// let the pre-check below short-circuit and skip the scrub entirely,
|
|
411
370
|
// silently leaving a token-bearing config on shared EFS -- the exact
|
|
412
371
|
// outcome this function exists to prevent. Fail closed and attempt the
|
|
413
372
|
// removal instead.
|
|
@@ -418,17 +377,12 @@ export class CmsWorker {
|
|
|
418
377
|
}
|
|
419
378
|
catch {
|
|
420
379
|
// ANY throw is 'unreadable', never 'absent'. The genuinely-absent case
|
|
421
|
-
// does not reach here at all
|
|
422
|
-
// against 3.36: it only treats a task as failed when stderr is
|
|
423
|
-
// non-empty, and a missing key writes nothing to stderr). So a throw
|
|
380
|
+
// does not reach here at all (simple-git resolves with ''), so a throw
|
|
424
381
|
// means something actually went wrong, and mapping that to "no token
|
|
425
|
-
// here"
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
// message is raw stdout+stderr with no exit-code text -- and its
|
|
430
|
-
// empty-message fallback mapped a hypothetical throw to 'absent',
|
|
431
|
-
// which is exactly the direction this must not fail.
|
|
382
|
+
// here" is the one fail-OPEN reading available. Classifying git's
|
|
383
|
+
// exit-1 "key not found" from the message text is not an option either:
|
|
384
|
+
// simple-git's GitError message is raw stdout+stderr with no exit-code
|
|
385
|
+
// text to match on.
|
|
432
386
|
return 'unreadable';
|
|
433
387
|
}
|
|
434
388
|
};
|
|
@@ -443,12 +397,12 @@ export class CmsWorker {
|
|
|
443
397
|
}
|
|
444
398
|
catch (err) {
|
|
445
399
|
// Reached only from the 'unreadable' path, where the remote may in fact
|
|
446
|
-
// not exist. Let the verification below decide rather than failing here
|
|
447
|
-
// it is the authoritative check
|
|
400
|
+
// not exist. Let the verification below decide rather than failing here;
|
|
401
|
+
// it is the authoritative check and it fails closed.
|
|
448
402
|
workerLogWarn(` removeRemote('origin') failed in ${gitDir}: ${getErrorMessage(err)} -- verifying directly`);
|
|
449
403
|
}
|
|
450
|
-
// Fails closed on BOTH a surviving URL and an unverifiable read:
|
|
451
|
-
//
|
|
404
|
+
// Fails closed on BOTH a surviving URL and an unverifiable read: without
|
|
405
|
+
// proof the token is gone from shared storage, do not proceed.
|
|
452
406
|
const remaining = await readOriginUrl();
|
|
453
407
|
if (remaining !== null) {
|
|
454
408
|
throw new Error(`Could not confirm the 'origin' remote is gone from ${gitDir} (${remaining === 'unreadable'
|
|
@@ -458,19 +412,17 @@ export class CmsWorker {
|
|
|
458
412
|
}
|
|
459
413
|
}
|
|
460
414
|
/**
|
|
461
|
-
* Ensure remote.git bare repo exists
|
|
462
|
-
*
|
|
415
|
+
* Ensure the remote.git bare repo exists, cloning it from GitHub on first
|
|
416
|
+
* run.
|
|
463
417
|
*
|
|
464
418
|
* Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
|
|
465
|
-
* commits, or a base branch
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* previous run could have left a poisoned remote.git behind before this
|
|
473
|
-
* guard existed.
|
|
419
|
+
* commits, or a base branch never pushed) exits 0 and produces a refs-less
|
|
420
|
+
* bare repo whose HEAD points at an unborn branch. `fs.stat` cannot tell that
|
|
421
|
+
* from a healthy clone, so left unchecked it silently poisons remote.git —
|
|
422
|
+
* every later branch operation breaks and the fs.stat short-circuit means it
|
|
423
|
+
* never heals. The base branch is therefore verified right after cloning AND
|
|
424
|
+
* on the already-exists fast path, since a previous run can have left a
|
|
425
|
+
* poisoned remote.git behind.
|
|
474
426
|
*/
|
|
475
427
|
async ensureRemoteGit() {
|
|
476
428
|
let exists;
|
|
@@ -482,32 +434,30 @@ export class CmsWorker {
|
|
|
482
434
|
exists = false;
|
|
483
435
|
}
|
|
484
436
|
if (exists) {
|
|
485
|
-
// SELF-HEAL, before anything else touches this repo
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
// "delete remote.git and restart" refusal below and so sat on EFS until
|
|
491
|
-
// an operator acted.
|
|
437
|
+
// SELF-HEAL, before anything else touches this repo: re-checked on every
|
|
438
|
+
// boot, not only at clone time, so a token that survived one scrub does
|
|
439
|
+
// not survive forever, and a clone interrupted between `git clone` and
|
|
440
|
+
// the scrub cannot leave a token-bearing config sitting on EFS until an
|
|
441
|
+
// operator acts.
|
|
492
442
|
await this.scrubPersistedRemote(this.remoteGitPath);
|
|
493
443
|
try {
|
|
494
444
|
await this.verifyBaseBranchExists(this.remoteGitPath);
|
|
495
445
|
}
|
|
496
446
|
catch (err) {
|
|
497
447
|
workerLogError(`remote.git base branch verification failed: ${getErrorMessage(err)}`);
|
|
498
|
-
// Do NOT auto-delete: an existing remote.git
|
|
499
|
-
// canopycms-settings-* branches or other state worth preserving
|
|
500
|
-
//
|
|
448
|
+
// Do NOT auto-delete: an existing remote.git can hold unpushed
|
|
449
|
+
// canopycms-settings-* branches or other state worth preserving, so
|
|
450
|
+
// deletion is the operator's call.
|
|
501
451
|
throw new Error(`remote.git at ${this.remoteGitPath} has no branch '${this.baseBranch}' (likely cloned while the GitHub repo was empty). Delete ${this.remoteGitPath} and restart the worker to re-clone.`);
|
|
502
452
|
}
|
|
503
453
|
return; // Already exists and has the base branch
|
|
504
454
|
}
|
|
505
455
|
workerLog('Initializing remote.git from GitHub...');
|
|
506
456
|
const git = simpleGit();
|
|
507
|
-
// Clone under a TEMP name and rename into place only once the token
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// directory, which the next boot deletes
|
|
457
|
+
// Clone under a TEMP name and rename into place only once the token is
|
|
458
|
+
// scrubbed and the repo verified, so `remote.git` never exists on EFS in a
|
|
459
|
+
// token-bearing state. A crash mid-clone leaves only this staging
|
|
460
|
+
// directory, which the next boot deletes, rather than a poisoned
|
|
511
461
|
// `remote.git` that fs.stat cannot distinguish from a healthy one.
|
|
512
462
|
const stagingPath = `${this.remoteGitPath}.cloning`;
|
|
513
463
|
await fs.rm(stagingPath, { recursive: true, force: true });
|
|
@@ -522,8 +472,8 @@ export class CmsWorker {
|
|
|
522
472
|
catch (err) {
|
|
523
473
|
workerLogError(`remote.git clone failed: ${redactCredentials(getErrorMessage(err))}`);
|
|
524
474
|
// Deleting before throwing is what makes this recoverable: the next
|
|
525
|
-
// start() sees no remote.git and re-clones, instead of
|
|
526
|
-
//
|
|
475
|
+
// start() sees no remote.git and re-clones, instead of sticking forever
|
|
476
|
+
// behind a poisoned bare repo fs.stat alone cannot detect.
|
|
527
477
|
await fs.rm(stagingPath, { recursive: true, force: true });
|
|
528
478
|
throw new Error(`remote.git clone of ${this.config.githubOwner}/${this.config.githubRepo} failed or has no branch '${this.baseBranch}' - the GitHub repository may be empty, or the base branch may not exist. Push an initial commit to '${this.baseBranch}' and restart the worker (systemd will retry automatically).`);
|
|
529
479
|
}
|
|
@@ -532,24 +482,18 @@ export class CmsWorker {
|
|
|
532
482
|
}
|
|
533
483
|
// --- Task-queue cluster (worker/task-runner.ts) ------------------------
|
|
534
484
|
//
|
|
535
|
-
// READ THIS BEFORE STUBBING ANY DELEGATOR BELOW
|
|
536
|
-
// and the difference is invisible from here.
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
// files can reach the implementations through the instance; production never
|
|
541
|
-
// dispatches through them, because the extracted modules call each other
|
|
542
|
-
// directly at module level.
|
|
485
|
+
// READ THIS BEFORE STUBBING ANY DELEGATOR BELOW: they are not all the same,
|
|
486
|
+
// and the difference is invisible from here. `processTaskQueue` is the public
|
|
487
|
+
// loop entry `scheduleLoop` drives, so it IS on the production path; the
|
|
488
|
+
// other three exist ONLY so test files can reach the implementations through
|
|
489
|
+
// the instance, since the extracted modules call each other at module level.
|
|
543
490
|
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// `
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
// If you need to stub a method that isn't on the context, add it to
|
|
551
|
-
// WorkerContext and route the internal caller through `ctx` -- do not assume
|
|
552
|
-
// the delegator's existence means the stub takes effect.
|
|
491
|
+
// So replacing one of these on an instance affects production behaviour only
|
|
492
|
+
// if the context also routes it. `executeTask` and `pushBranchToGitHub` are
|
|
493
|
+
// on `WorkerContext` for exactly that reason; `updateBranchMetadata` is NOT,
|
|
494
|
+
// so a stub installed there is a silent no-op. To stub a method that is not
|
|
495
|
+
// on the context, add it to WorkerContext and route the internal caller
|
|
496
|
+
// through `ctx` -- a delegator's existence does not make a stub take effect.
|
|
553
497
|
async processTaskQueue() {
|
|
554
498
|
return processTaskQueue(this.ctx());
|
|
555
499
|
}
|
|
@@ -567,16 +511,11 @@ export class CmsWorker {
|
|
|
567
511
|
* Octokit client from it.
|
|
568
512
|
*
|
|
569
513
|
* DEFERRED out of the constructor deliberately, exactly as
|
|
570
|
-
* `ensureSettingsBranch()` is
|
|
514
|
+
* `ensureSettingsBranch()` is and for the reason that method records:
|
|
571
515
|
* `resolveWorkerGitHubAuth` throws for a half-configured credential (both
|
|
572
|
-
* set, neither set, an unusable mint timeout or refresh interval), and a
|
|
573
|
-
* `new CmsWorker(...)` lands BEFORE the only code that writes
|
|
574
|
-
* `lastFatalError` — start()'s catch.
|
|
575
|
-
* worker and calls start() separately, and its `main().catch()` only logs
|
|
576
|
-
* and exits, so a constructor throw is an invisible ~5s systemd crash-loop
|
|
577
|
-
* that `cdk deploy` reports as success while the admin panel shows the
|
|
578
|
-
* worker 'absent' with no fatal error to explain it. That is a shipped
|
|
579
|
-
* regression this codebase has already paid for once (#198).
|
|
516
|
+
* set, neither set, an unusable mint timeout or refresh interval), and a
|
|
517
|
+
* throw during `new CmsWorker(...)` lands BEFORE the only code that writes
|
|
518
|
+
* `lastFatalError` — start()'s catch.
|
|
580
519
|
*
|
|
581
520
|
* Idempotent, and it does NOT replace an `octokit` a test has already
|
|
582
521
|
* assigned onto the instance — see the field's comment.
|
|
@@ -591,34 +530,29 @@ export class CmsWorker {
|
|
|
591
530
|
return this.githubAuth;
|
|
592
531
|
}
|
|
593
532
|
/**
|
|
594
|
-
* The Octokit client, built on first use.
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
* exactly that case — apps/test-app's e2e route calls it directly, and its
|
|
600
|
-
* `pollMergeState` dispatch reads `ctx.octokit()`.
|
|
533
|
+
* The Octokit client, built on first use. Every read goes through here rather
|
|
534
|
+
* than touching the field, which the constructor does not populate: a method
|
|
535
|
+
* reached without start() would otherwise see `undefined`.
|
|
536
|
+
* `rebaseActiveBranches()` is exactly that case — apps/test-app's e2e route
|
|
537
|
+
* calls it directly, and its `pollMergeState` dispatch reads `ctx.octokit()`.
|
|
601
538
|
*/
|
|
602
539
|
octokitClient() {
|
|
603
540
|
this.ensureGitHubAuth();
|
|
604
541
|
return this.octokit;
|
|
605
542
|
}
|
|
606
543
|
/**
|
|
607
|
-
* The single seam through which every git-over-HTTPS credential reaches a
|
|
608
|
-
*
|
|
609
|
-
*
|
|
544
|
+
* The single seam through which every git-over-HTTPS credential reaches a git
|
|
545
|
+
* command. The only other consumer is Octokit, built from the same resolution
|
|
546
|
+
* by `ensureGitHubAuth()` above.
|
|
610
547
|
*
|
|
611
|
-
* Async because
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
* token is near expiry, so the usual cost is a resolved microtask, and the
|
|
617
|
-
* token path is a bare `async` return.
|
|
548
|
+
* Async because under GitHub App auth `resolveGitToken` mints an installation
|
|
549
|
+
* token lasting about an hour. NOTHING may cache what this returns — a URL
|
|
550
|
+
* built from an installation token goes stale with it — and resolving per use
|
|
551
|
+
* is cheap, since `@octokit/auth-app` answers from its own cache until the
|
|
552
|
+
* token is near expiry.
|
|
618
553
|
*
|
|
619
554
|
* A mint failure propagates AS THROWN, carrying the `.status` that
|
|
620
|
-
* `isPermanentTaskFailure`
|
|
621
|
-
* github-auth.ts.
|
|
555
|
+
* `isPermanentTaskFailure` classifies on — see github-auth.ts.
|
|
622
556
|
*
|
|
623
557
|
* Do NOT add a parallel token accessor alongside it. Every instance-backed
|
|
624
558
|
* WorkerContext member stays a function precisely so tests can replace it
|
|
@@ -632,36 +566,31 @@ export class CmsWorker {
|
|
|
632
566
|
/**
|
|
633
567
|
* Prove the GitHub App credential works before anything depends on it.
|
|
634
568
|
*
|
|
635
|
-
* Without this the first failure comes out of `ensureRemoteGit`'s bare
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* initial `syncGit`), so the worker starts and its loops retry. Rethrowing
|
|
652
|
-
* every error class would make the App path exit instead, and systemd
|
|
653
|
-
* (Restart=always) would crash-loop it until GitHub recovered — each
|
|
654
|
-
* iteration telling the operator to go and check their private key.
|
|
569
|
+
* Without this the first failure comes out of `ensureRemoteGit`'s bare clone
|
|
570
|
+
* below, whose catch blames the repository ("may be empty, or the base branch
|
|
571
|
+
* may not exist") and sends an operator holding a bad private key looking for
|
|
572
|
+
* a problem that does not exist. Called from start()'s try, so the failure is
|
|
573
|
+
* also recorded as `lastFatalError` and reaches the admin panel. No-op on the
|
|
574
|
+
* token path: a PAT is a literal, so the first real request checks everything
|
|
575
|
+
* this could.
|
|
576
|
+
*
|
|
577
|
+
* FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves are
|
|
578
|
+
* load-bearing. Not always fatal, because the two credential paths must
|
|
579
|
+
* degrade alike: on the token path a GitHub 502 during boot is absorbed (a
|
|
580
|
+
* warm `remote.git` short-circuits `ensureRemoteGit`, `Promise.allSettled`
|
|
581
|
+
* swallows the initial `syncGit`) so the worker starts and its loops retry,
|
|
582
|
+
* while rethrowing every error class would make the App path exit and systemd
|
|
583
|
+
* crash-loop it until GitHub recovered — each iteration telling the operator
|
|
584
|
+
* to check their private key.
|
|
655
585
|
*
|
|
656
586
|
* But fail CLOSED, via `isTransientAuthFailure` rather than the inverse of
|
|
657
|
-
* `isPermanentTaskFailure
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* isTransientAuthFailure in github-auth.ts.
|
|
587
|
+
* `isPermanentTaskFailure`: that classifier defaults a status-less error to
|
|
588
|
+
* transient, which is right on the task path (bounded by `maxRetries`) and
|
|
589
|
+
* wrong here (bounded by nothing). A key that never reaches GitHub at all —
|
|
590
|
+
* the wrong type, or too mangled to sign with — fails locally and
|
|
591
|
+
* status-lessly, so defaulting to transient would boot a worker with a dead
|
|
592
|
+
* credential, record no `lastFatalError`, and show healthy in the admin panel
|
|
593
|
+
* while every task and every sync failed.
|
|
665
594
|
*/
|
|
666
595
|
async preflightGitHubAppAuth() {
|
|
667
596
|
if (!this.config.githubAppAuth)
|
|
@@ -677,30 +606,29 @@ export class CmsWorker {
|
|
|
677
606
|
'Continuing — this looks transient, and the credential is minted again on first use.');
|
|
678
607
|
return;
|
|
679
608
|
}
|
|
680
|
-
// Re-thrown
|
|
681
|
-
// preserve the error identity for task classification. Nothing
|
|
682
|
-
//
|
|
683
|
-
//
|
|
609
|
+
// Re-thrown WITH context, unlike buildGitHubUrl() above, which must
|
|
610
|
+
// preserve the error identity for task classification. Nothing classifies
|
|
611
|
+
// a startup failure -- start()'s catch records the message and the
|
|
612
|
+
// process exits -- so the operator-facing wording wins here.
|
|
684
613
|
throw new Error(`GitHub App authentication failed: ${detail}. ` +
|
|
685
614
|
'Check the app id, the installation id, and that the private key belongs to that app.');
|
|
686
615
|
}
|
|
687
616
|
workerLog('GitHub App authentication verified');
|
|
688
617
|
}
|
|
689
618
|
/**
|
|
690
|
-
* The workspace directory for a branch named by its GIT REF name -- the
|
|
691
|
-
*
|
|
619
|
+
* The workspace directory for a branch named by its GIT REF name -- the form
|
|
620
|
+
* task payloads carry (`context.branch.name`), not the directory form.
|
|
692
621
|
*
|
|
693
622
|
* These differ for any name outside `[A-Za-z0-9._-]`, `/` being the obvious
|
|
694
623
|
* one: workspaces are provisioned under `sanitizeBranchName(...)` (see
|
|
695
624
|
* paths/branch.ts's `resolveBranchPaths`), so `feature/x` lives in
|
|
696
|
-
* `feature-x`. Joining the
|
|
697
|
-
* that does not exist -- which
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* wedging exactly the branch this workstream exists to unwedge.
|
|
625
|
+
* `feature-x`. Joining the RAW name instead silently addresses a directory
|
|
626
|
+
* that does not exist -- which drops the metadata writers' updates, and makes
|
|
627
|
+
* the leased push read the history-rewrite marker as absent and go out
|
|
628
|
+
* unleased, wedging the branch.
|
|
701
629
|
*
|
|
702
|
-
* `name` inside the metadata itself stays the raw ref name; only the path
|
|
703
|
-
*
|
|
630
|
+
* `name` inside the metadata itself stays the raw ref name; only the path is
|
|
631
|
+
* sanitized.
|
|
704
632
|
*/
|
|
705
633
|
branchWorkspacePath(branchRefName) {
|
|
706
634
|
return path.join(this.contentBranchesPath, sanitizeBranchName(branchRefName));
|
|
@@ -711,30 +639,26 @@ export class CmsWorker {
|
|
|
711
639
|
* `checkout --theirs` resolution loop overwrites them. No-op in production.
|
|
712
640
|
*
|
|
713
641
|
* A test subclass overrides this to land a real `ContentStore` write at
|
|
714
|
-
* exactly the instant the rebase is mid-flight
|
|
715
|
-
*
|
|
716
|
-
* shell rendezvous. See the "Deterministic interleavings" testing pattern in
|
|
642
|
+
* exactly the instant the rebase is mid-flight, with no sleeps or shell
|
|
643
|
+
* rendezvous. See the "Deterministic interleavings" pattern in
|
|
717
644
|
* docs/concurrency.md, and `ContentStore.afterPrePassForTesting()` for the
|
|
718
645
|
* same idiom on the write side.
|
|
719
646
|
*/
|
|
720
647
|
async afterConflictDetectedForTesting() { }
|
|
721
648
|
/**
|
|
722
|
-
* Test seam, sibling of {@link afterConflictDetectedForTesting}: runs
|
|
649
|
+
* Test seam, sibling of {@link afterConflictDetectedForTesting}: runs the
|
|
723
650
|
* instant a rebase round has succeeded, before the completion path (cache
|
|
724
|
-
* invalidation, conflict metadata, [SYNC-H1] marker) executes
|
|
725
|
-
*
|
|
726
|
-
*
|
|
651
|
+
* invalidation, conflict metadata, [SYNC-H1] marker) executes, so a test can
|
|
652
|
+
* lose the content-write lock at exactly the point where bailing out would
|
|
653
|
+
* strand a rewritten history.
|
|
727
654
|
*/
|
|
728
655
|
async afterRebaseCompletedForTesting() { }
|
|
729
656
|
// --- Rebase loop (worker/rebase.ts) ------------------------------------
|
|
730
657
|
//
|
|
731
658
|
// Both are TEST-ONLY entry points -- see the task-queue block above. Nothing
|
|
732
|
-
// in production dispatches through either
|
|
733
|
-
// directly and that calls `pollMergeState` directly.
|
|
734
|
-
//
|
|
735
|
-
// drives), and cms-worker-merge-poll.test.ts calls `pollMergeState` with a
|
|
736
|
-
// mocked `octokit`. Neither is on WorkerContext, so a stub installed on
|
|
737
|
-
// either is a no-op as far as a production code path is concerned.
|
|
659
|
+
// in production dispatches through either: `syncGit` calls `runRebaseCycle`
|
|
660
|
+
// directly and that calls `pollMergeState` directly. Neither is on
|
|
661
|
+
// WorkerContext, so a stub installed on either is a no-op for production.
|
|
738
662
|
//
|
|
739
663
|
// `rebaseActiveBranches` also has a consumer outside the test files:
|
|
740
664
|
// apps/test-app/app/api/e2e-test/rebase/route.ts drives it from an e2e
|
|
@@ -746,14 +670,11 @@ export class CmsWorker {
|
|
|
746
670
|
//
|
|
747
671
|
// `syncGit` is the public loop entry `scheduleLoop` drives. The three private
|
|
748
672
|
// ones below it are TEST-ONLY entry points -- see the task-queue block above
|
|
749
|
-
// -- each called directly by one test file
|
|
750
|
-
// settings push, cms-worker-base-refresh for the base workspace,
|
|
751
|
-
// cms-worker.test.ts for the trash sweep). `syncGit` reaches all three as
|
|
673
|
+
// -- each called directly by one test file. `syncGit` reaches all three as
|
|
752
674
|
// module-level calls, so a stub installed on one of these is a no-op.
|
|
753
675
|
//
|
|
754
676
|
// `reconcileTrackedBranches` deliberately has NO delegator: no test reaches
|
|
755
|
-
// it through the instance.
|
|
756
|
-
// this file are stale by definition -- it is in git-sync.ts.
|
|
677
|
+
// it through the instance.
|
|
757
678
|
async syncGit() {
|
|
758
679
|
return syncGit(this.ctx());
|
|
759
680
|
}
|
|
@@ -762,10 +683,10 @@ export class CmsWorker {
|
|
|
762
683
|
*
|
|
763
684
|
* One of `refreshGitHubCredential`'s two call sites, and the one that works
|
|
764
685
|
* when nobody is publishing: it fetches from GitHub every `gitSyncInterval`
|
|
765
|
-
*
|
|
766
|
-
*
|
|
686
|
+
* whether or not anyone is editing, so a credential that has stopped working
|
|
687
|
+
* surfaces here even with no push queued for days.
|
|
767
688
|
*
|
|
768
|
-
* The
|
|
689
|
+
* The SYNC failure is what propagates to `scheduleLoop`'s catch;
|
|
769
690
|
* `refreshGitHubCredential` never throws, so nothing it does can replace it.
|
|
770
691
|
*/
|
|
771
692
|
async syncGitWithCredentialRefresh() {
|
|
@@ -783,42 +704,29 @@ export class CmsWorker {
|
|
|
783
704
|
* **Two call sites, each covering what the other cannot.** The git-sync loop
|
|
784
705
|
* (`syncGitWithCredentialRefresh`) notices a dead credential when nobody is
|
|
785
706
|
* publishing. `processTaskQueue`'s per-task catch is what saves a publish: a
|
|
786
|
-
* push task spends its retry budget on a 5s/10s/20s backoff
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* so a
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
* `
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
* `
|
|
808
|
-
*
|
|
809
|
-
* floors, so a read issued by one throttles the other.
|
|
810
|
-
*
|
|
811
|
-
* **Never throws.** Both callers are already reporting a failure, and that
|
|
812
|
-
* failure is the one that must reach the log.
|
|
813
|
-
*
|
|
814
|
-
* **Bounded by `taskTimeoutMs`**, because the task loop awaits it, and a read
|
|
815
|
-
* that never settled would stop every publish queued behind it. An adopter's
|
|
816
|
-
* provider may have no bound at all, and the AWS one, bounded as it is
|
|
817
|
-
* (canopycms-cdk/worker/secrets.ts), can still take up to 87s for one
|
|
818
|
-
* `getSecret` — longer than the 60s default here. A read that loses the race
|
|
819
|
-
* is not cancelled, and
|
|
820
|
-
* may still land later; `refreshCredential` discards a result older than one
|
|
821
|
-
* it has already applied, so a late landing cannot put a stale token back.
|
|
707
|
+
* push task spends its retry budget on a 5s/10s/20s backoff, well inside one
|
|
708
|
+
* 5-minute sync interval, so with the sync loop as the only trigger a publish
|
|
709
|
+
* meeting a rotated token fails permanently while the working one is already
|
|
710
|
+
* in the secret store. Every consumer reaches the credential through
|
|
711
|
+
* `ensureGitHubAuth()`, which reads it per use, so a refresh from either site
|
|
712
|
+
* repairs all of them for their NEXT use — but not an attempt already failed.
|
|
713
|
+
*
|
|
714
|
+
* NOT gated on the error looking auth-shaped, at either site: a `git
|
|
715
|
+
* fetch`/`push` rejected for a dead token throws a plain simple-git error
|
|
716
|
+
* (exit 128, no HTTP `.status`) that `isPermanentTaskFailure` reads as
|
|
717
|
+
* transient, so a gate keyed on it would never fire. Two floors bound the
|
|
718
|
+
* cost instead — core's `refreshGitHubTokenMinIntervalMs` (default 60s,
|
|
719
|
+
* enforced by `refreshCredential` in github-auth.ts) and whatever floor the
|
|
720
|
+
* provider keeps (the AWS one reads at most once per five minutes) — and both
|
|
721
|
+
* call sites share both, so a read issued by one throttles the other. On the
|
|
722
|
+
* GitHub App path the refresh is a no-op.
|
|
723
|
+
*
|
|
724
|
+
* **Never throws**: both callers are already reporting the failure that must
|
|
725
|
+
* reach the log. **Bounded by `taskTimeoutMs`**, because the task loop awaits
|
|
726
|
+
* it and a read that never settled would stop every publish queued behind it
|
|
727
|
+
* (an adopter's provider may have no bound at all; the AWS one can take 87s
|
|
728
|
+
* for one `getSecret`). A losing read is not cancelled and may land later, but
|
|
729
|
+
* `refreshCredential` discards a result older than one already applied.
|
|
822
730
|
*/
|
|
823
731
|
async refreshGitHubCredential() {
|
|
824
732
|
let timer;
|
|
@@ -832,8 +740,8 @@ export class CmsWorker {
|
|
|
832
740
|
}
|
|
833
741
|
catch (err) {
|
|
834
742
|
// [REDACT] The message can name the secret and, on a malformed-secret
|
|
835
|
-
// path, quote what was read. Console only, but the rule
|
|
836
|
-
//
|
|
743
|
+
// path, quote what was read. Console only, but the rule is uniform --
|
|
744
|
+
// see redactCredentials in utils/error.ts.
|
|
837
745
|
workerLogError('Failed to re-read the GitHub credential after a failure:', redactCredentials(getErrorMessage(err)));
|
|
838
746
|
}
|
|
839
747
|
finally {
|