canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a create of a branch name its own creator just made answers with that branch rather
|
|
3
|
+
* than a 409, so a request killed between publishing the branch and responding is safe to retry.
|
|
4
|
+
* Dependency-free: the editor applies the same rule when it settles an unanswered create.
|
|
5
|
+
* @internal Exported for tests.
|
|
6
|
+
*/
|
|
7
|
+
export const IDEMPOTENT_CREATE_WINDOW_MS = 5 * 60_000;
|
|
8
|
+
/** Whether `branch` can be the result of `userId`'s create: theirs, and inside the window. */
|
|
9
|
+
export function isCreatorsRecentBranch(branch, userId) {
|
|
10
|
+
return (userId !== undefined &&
|
|
11
|
+
branch.createdBy === userId &&
|
|
12
|
+
Date.now() - Date.parse(branch.createdAt) < IDEMPOTENT_CREATE_WINDOW_MS);
|
|
13
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type
|
|
2
|
+
import { type BranchListItemResponse } from './branch.js';
|
|
3
3
|
export declare const requestChanges: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
4
4
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
5
5
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -10,7 +10,7 @@ export declare const requestChanges: import("./route-builder.js").RouteDefinitio
|
|
|
10
10
|
branch: string & {
|
|
11
11
|
readonly __brand: "BranchName";
|
|
12
12
|
};
|
|
13
|
-
}>, undefined,
|
|
13
|
+
}>, undefined, BranchListItemResponse>;
|
|
14
14
|
export declare const approveBranch: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
15
15
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
16
16
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -21,4 +21,4 @@ export declare const approveBranch: import("./route-builder.js").RouteDefinition
|
|
|
21
21
|
branch: string & {
|
|
22
22
|
readonly __brand: "BranchName";
|
|
23
23
|
};
|
|
24
|
-
}>, undefined,
|
|
24
|
+
}>, undefined, BranchListItemResponse>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { branchParamSchema } from './validators.js';
|
|
2
|
+
import { toBranchListItem } from './branch.js';
|
|
2
3
|
import { getBranchMetadataFileManager } from '../branch-metadata.js';
|
|
3
4
|
import { defineEndpoint } from './route-builder.js';
|
|
4
5
|
import { syncConvertToDraft } from './github-sync.js';
|
|
@@ -17,9 +18,13 @@ const requestChangesHandler = async (gc, ctx, _req, _params) => {
|
|
|
17
18
|
branch: { name: branchContext.branch.name, status: 'editing' },
|
|
18
19
|
});
|
|
19
20
|
// TODO: Optionally record comment in .canopycms/comments.json when comment system is implemented
|
|
20
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
|
|
25
|
+
};
|
|
21
26
|
};
|
|
22
|
-
const approveBranchHandler = async (gc,
|
|
27
|
+
const approveBranchHandler = async (gc, ctx, _req, _params) => {
|
|
23
28
|
const { branchContext } = gc;
|
|
24
29
|
if (branchContext.branch.status !== 'submitted') {
|
|
25
30
|
return {
|
|
@@ -33,7 +38,11 @@ const approveBranchHandler = async (gc, _ctx, _req, _params) => {
|
|
|
33
38
|
branch: { name: branchContext.branch.name, status: 'approved' },
|
|
34
39
|
});
|
|
35
40
|
// TODO: Optionally call githubService.approvePullRequest() when GitHub integration is needed
|
|
36
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
status: 200,
|
|
44
|
+
data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
|
|
45
|
+
};
|
|
37
46
|
};
|
|
38
47
|
export const requestChanges = defineEndpoint({
|
|
39
48
|
namespace: 'workflow',
|
|
@@ -43,7 +52,7 @@ export const requestChanges = defineEndpoint({
|
|
|
43
52
|
params: branchParamSchema,
|
|
44
53
|
// body/bodyType removed: comment field was declared but never stored.
|
|
45
54
|
// Re-add when comment storage is implemented (see TODO in handler).
|
|
46
|
-
responseType: '
|
|
55
|
+
responseType: 'BranchListItemResponse',
|
|
47
56
|
response: {},
|
|
48
57
|
defaultMockData: {
|
|
49
58
|
branch: {
|
|
@@ -64,7 +73,7 @@ export const approveBranch = defineEndpoint({
|
|
|
64
73
|
method: 'POST',
|
|
65
74
|
path: '/:branch/approve',
|
|
66
75
|
params: branchParamSchema,
|
|
67
|
-
responseType: '
|
|
76
|
+
responseType: 'BranchListItemResponse',
|
|
68
77
|
response: {},
|
|
69
78
|
defaultMockData: {
|
|
70
79
|
branch: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type
|
|
2
|
+
import { type BranchListItemResponse, type BranchResponse } from './branch.js';
|
|
3
3
|
export type { BranchMergeResponse } from './branch-merge.js';
|
|
4
4
|
export declare const WORKFLOW_ROUTES: {
|
|
5
5
|
readonly getStatus: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
@@ -23,7 +23,7 @@ export declare const WORKFLOW_ROUTES: {
|
|
|
23
23
|
branch: string & {
|
|
24
24
|
readonly __brand: "BranchName";
|
|
25
25
|
};
|
|
26
|
-
}>, undefined,
|
|
26
|
+
}>, undefined, BranchListItemResponse>;
|
|
27
27
|
readonly withdraw: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
28
28
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
29
29
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -34,7 +34,7 @@ export declare const WORKFLOW_ROUTES: {
|
|
|
34
34
|
branch: string & {
|
|
35
35
|
readonly __brand: "BranchName";
|
|
36
36
|
};
|
|
37
|
-
}>, undefined,
|
|
37
|
+
}>, undefined, BranchListItemResponse>;
|
|
38
38
|
readonly requestChanges: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
39
39
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
40
40
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -45,7 +45,7 @@ export declare const WORKFLOW_ROUTES: {
|
|
|
45
45
|
branch: string & {
|
|
46
46
|
readonly __brand: "BranchName";
|
|
47
47
|
};
|
|
48
|
-
}>, undefined,
|
|
48
|
+
}>, undefined, BranchListItemResponse>;
|
|
49
49
|
readonly approve: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
50
50
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
51
51
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -56,7 +56,7 @@ export declare const WORKFLOW_ROUTES: {
|
|
|
56
56
|
branch: string & {
|
|
57
57
|
readonly __brand: "BranchName";
|
|
58
58
|
};
|
|
59
|
-
}>, undefined,
|
|
59
|
+
}>, undefined, BranchListItemResponse>;
|
|
60
60
|
readonly markMerged: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
61
61
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
62
62
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { branchParamSchema } from './validators.js';
|
|
2
|
+
import { toBranchListItem } from './branch.js';
|
|
2
3
|
import { getBranchMetadataFileManager } from '../branch-metadata.js';
|
|
3
4
|
import { withdrawBranch } from './branch-withdraw.js';
|
|
4
5
|
import { requestChanges, approveBranch } from './branch-review.js';
|
|
@@ -8,6 +9,8 @@ import { canPerformWorkflowAction, getBranchProtection } from '../authorization/
|
|
|
8
9
|
import { syncSubmitPr } from './github-sync.js';
|
|
9
10
|
import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from '../utils/error.js';
|
|
10
11
|
import { isNonFastForwardRejection } from '../utils/git.js';
|
|
12
|
+
import { ContentWriteLockBusyError } from '../utils/content-write-lock.js';
|
|
13
|
+
import { submissionEditorFromUser } from '../submission-attribution.js';
|
|
11
14
|
const getBranchStatusHandler = async (gc, _ctx, _req, _params) => {
|
|
12
15
|
const { branchContext } = gc;
|
|
13
16
|
return { ok: true, status: 200, data: { branch: branchContext.branch } };
|
|
@@ -49,10 +52,24 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
// Commit and push changes
|
|
55
|
+
const submitter = submissionEditorFromUser(req.user);
|
|
56
|
+
let changedPaths;
|
|
52
57
|
try {
|
|
53
|
-
|
|
58
|
+
;
|
|
59
|
+
({ changedPaths } = await ctx.services.submitBranch({ context: branchContext, submitter }));
|
|
54
60
|
}
|
|
55
61
|
catch (err) {
|
|
62
|
+
// Retriable either way: a resubmit commits nothing new and pushes only
|
|
63
|
+
// what has not reached the remote.
|
|
64
|
+
if (err instanceof ContentWriteLockBusyError) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
status: 409,
|
|
68
|
+
error: err.outcome === 'not-run'
|
|
69
|
+
? `Could not submit "${branchContext.branch.name}" right now: it is being synced with its base branch, or a save is in flight. Nothing was submitted; try again in a moment.`
|
|
70
|
+
: `"${branchContext.branch.name}" was being synced while it was submitted, so the submit may not have completed. Try submitting again.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
56
73
|
const message = getErrorMessage(err);
|
|
57
74
|
// Full path detail (including branchRoot, an absolute path) to server logs
|
|
58
75
|
// only; the client only ever sees the sanitized form (API-H2). Credentials
|
|
@@ -62,8 +79,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
62
79
|
// A non-fast-forward rejection means this branch and the deployment's
|
|
63
80
|
// local repository have diverged. Retrying the identical push can never
|
|
64
81
|
// succeed (see isNonFastForwardRejection), so surface 409 instead of the
|
|
65
|
-
// generic 500 below. Everything else (network, auth
|
|
66
|
-
// keeps the existing 500 path unchanged.
|
|
82
|
+
// generic 500 below. Everything else (network, auth) keeps the 500 path.
|
|
67
83
|
//
|
|
68
84
|
// This push targets the deployment's OWN local origin (remote.git), not
|
|
69
85
|
// GitHub, so the message deliberately states only the observable fact and
|
|
@@ -91,7 +107,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
91
107
|
};
|
|
92
108
|
}
|
|
93
109
|
// Create or update PR (sync via githubService, or async via task queue)
|
|
94
|
-
const prResult = await syncSubmitPr(ctx, branchContext);
|
|
110
|
+
const prResult = await syncSubmitPr(ctx, branchContext, { submitter, changedPaths });
|
|
95
111
|
// Update metadata with status and PR info
|
|
96
112
|
const meta = getBranchMetadataFileManager(branchContext.branchRoot, branchContext.baseRoot);
|
|
97
113
|
const updated = await meta.save({
|
|
@@ -107,7 +123,11 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
|
|
|
107
123
|
rebaseFailure: undefined,
|
|
108
124
|
},
|
|
109
125
|
});
|
|
110
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
status: 200,
|
|
129
|
+
data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
|
|
130
|
+
};
|
|
111
131
|
};
|
|
112
132
|
const getBranchStatus = defineEndpoint({
|
|
113
133
|
namespace: 'workflow',
|
|
@@ -136,7 +156,7 @@ const submitBranchForMerge = defineEndpoint({
|
|
|
136
156
|
method: 'POST',
|
|
137
157
|
path: '/:branch/submit',
|
|
138
158
|
params: branchParamSchema,
|
|
139
|
-
responseType: '
|
|
159
|
+
responseType: 'BranchListItemResponse',
|
|
140
160
|
response: {},
|
|
141
161
|
defaultMockData: {
|
|
142
162
|
branch: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type
|
|
2
|
+
import { type BranchListItemResponse } from './branch.js';
|
|
3
3
|
/**
|
|
4
4
|
* Withdraw a submitted branch, converting the PR to draft and unlocking editing
|
|
5
5
|
*
|
|
@@ -19,4 +19,4 @@ export declare const withdrawBranch: import("./route-builder.js").RouteDefinitio
|
|
|
19
19
|
branch: string & {
|
|
20
20
|
readonly __brand: "BranchName";
|
|
21
21
|
};
|
|
22
|
-
}>, undefined,
|
|
22
|
+
}>, undefined, BranchListItemResponse>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { branchParamSchema } from './validators.js';
|
|
2
|
+
import { toBranchListItem } from './branch.js';
|
|
2
3
|
import { getBranchMetadataFileManager } from '../branch-metadata.js';
|
|
3
4
|
import { defineEndpoint } from './route-builder.js';
|
|
4
5
|
import { canPerformWorkflowAction, getBranchProtection } from '../authorization/index.js';
|
|
@@ -60,7 +61,11 @@ const withdrawBranchHandler = async (gc, ctx, req, _params) => {
|
|
|
60
61
|
: {}),
|
|
61
62
|
},
|
|
62
63
|
});
|
|
63
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
status: 200,
|
|
67
|
+
data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
|
|
68
|
+
};
|
|
64
69
|
};
|
|
65
70
|
/**
|
|
66
71
|
* Withdraw a submitted branch, converting the PR to draft and unlocking editing
|
|
@@ -77,7 +82,7 @@ export const withdrawBranch = defineEndpoint({
|
|
|
77
82
|
method: 'POST',
|
|
78
83
|
path: '/:branch/withdraw',
|
|
79
84
|
params: branchParamSchema,
|
|
80
|
-
responseType: '
|
|
85
|
+
responseType: 'BranchListItemResponse',
|
|
81
86
|
response: {},
|
|
82
87
|
defaultMockData: {
|
|
83
88
|
branch: {
|
package/dist/api/branch.d.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface BranchListItem extends BranchMetadata {
|
|
|
45
45
|
*/
|
|
46
46
|
submitBlocked?: boolean;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Response type for branch creation and the workflow transitions. Carries the
|
|
50
|
+
* list-item shape (server-computed flags included) so the editor can show the
|
|
51
|
+
* result without waiting for a listing that may lag behind it.
|
|
52
|
+
*/
|
|
53
|
+
export type BranchListItemResponse = ApiResponse<{
|
|
54
|
+
branch: BranchListItem;
|
|
55
|
+
}>;
|
|
48
56
|
/** Response type for listing branches */
|
|
49
57
|
export type BranchListResponse = ApiResponse<{
|
|
50
58
|
branches: BranchListItem[];
|
|
@@ -129,8 +137,13 @@ export interface CreateBranchBody {
|
|
|
129
137
|
description?: string;
|
|
130
138
|
access?: BranchMetadata['access'];
|
|
131
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Attach server-computed protected-base-branch flags to a branch. Reads config
|
|
142
|
+
* per call so dev-mode refreshActiveBranch() updates are reflected.
|
|
143
|
+
*/
|
|
144
|
+
export declare const toBranchListItem: (config: ApiContext["services"]["config"], branch: BranchMetadata) => BranchListItem;
|
|
132
145
|
/** @internal Exported for tests. */
|
|
133
|
-
export declare const createBranchHandler: (ctx: ApiContext, req: ApiRequest, body: z.infer<typeof createBranchBodySchema>) => Promise<
|
|
146
|
+
export declare const createBranchHandler: (ctx: ApiContext, req: ApiRequest, body: z.infer<typeof createBranchBodySchema>) => Promise<BranchListItemResponse>;
|
|
134
147
|
/** @internal Exported for tests. */
|
|
135
148
|
export declare const listBranchesHandler: (ctx: ApiContext, req: ApiRequest) => Promise<BranchListResponse>;
|
|
136
149
|
/** @internal Exported for tests. */
|
|
@@ -187,7 +200,7 @@ export declare const BRANCH_ROUTES: {
|
|
|
187
200
|
allowedUsers?: string[] | undefined;
|
|
188
201
|
allowedGroups?: string[] | undefined;
|
|
189
202
|
} | undefined;
|
|
190
|
-
}>,
|
|
203
|
+
}>, BranchListItemResponse>;
|
|
191
204
|
readonly delete: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
192
205
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
193
206
|
}, "strip", z.ZodTypeAny, {
|
package/dist/api/branch.js
CHANGED
|
@@ -2,16 +2,19 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { BranchWorkspaceManager } from '../branch-workspace.js';
|
|
5
|
+
import { BranchDirOccupiedError, BranchProvisioningBusyError, deletingDirName, removeLeftoverDir, } from '../branch-provisioning.js';
|
|
5
6
|
import { getBranchMetadataFileManager } from '../branch-metadata.js';
|
|
6
7
|
import { withOccFileLock } from '../utils/occ-json-write.js';
|
|
8
|
+
import { isCreatorsRecentBranch } from './branch-create-window.js';
|
|
7
9
|
import { defineEndpoint } from './route-builder.js';
|
|
8
10
|
import { createDebugLogger } from '../utils/debug.js';
|
|
9
11
|
import { clientOperatingStrategy } from '../operating-mode/index.js';
|
|
10
12
|
import { isNotFoundError, getErrorMessage, sanitizeErrorMessage } from '../utils/error.js';
|
|
11
13
|
import { filePathExists } from '../utils/fs.js';
|
|
12
14
|
import { isNetworkRemoteUrl } from '../utils/git.js';
|
|
13
|
-
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, } from '../paths/index.js';
|
|
15
|
+
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, isSettingsBranchName, } from '../paths/index.js';
|
|
14
16
|
import { GitManager } from '../git-manager.js';
|
|
17
|
+
import { syncDeleteRemoteBranch } from './github-sync.js';
|
|
15
18
|
import { branchNameSchema, branchParamSchema } from './validators.js';
|
|
16
19
|
const log = createDebugLogger({ prefix: 'BranchAPI' });
|
|
17
20
|
const createBranchBodySchema = z.object({
|
|
@@ -87,6 +90,20 @@ const resolveReadOnlyMirrorPath = (ctx, strategy) => {
|
|
|
87
90
|
process.env[remoteUrlConfig.envVarName] ??
|
|
88
91
|
remoteUrlConfig.autoDetectRemotePath);
|
|
89
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Attach server-computed protected-base-branch flags to a branch. Reads config
|
|
95
|
+
* per call so dev-mode refreshActiveBranch() updates are reflected.
|
|
96
|
+
*/
|
|
97
|
+
export const toBranchListItem = (config, branch) => {
|
|
98
|
+
const protection = getBranchWriteProtection(config, branch.name, branch.baseBranch, branch.status);
|
|
99
|
+
return {
|
|
100
|
+
...branch,
|
|
101
|
+
isProtected: protection.isProtected,
|
|
102
|
+
readOnly: protection.readOnly,
|
|
103
|
+
writeBlocked: protection.writeBlocked,
|
|
104
|
+
submitBlocked: protection.submitBlockedIncludingStatus,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
90
107
|
/** @internal Exported for tests. */
|
|
91
108
|
export const createBranchHandler = async (ctx, req, body) => {
|
|
92
109
|
return log.timed('api', 'createBranch', async () => {
|
|
@@ -95,15 +112,12 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
95
112
|
branchName,
|
|
96
113
|
userId: req.user.userId,
|
|
97
114
|
});
|
|
98
|
-
// Scope note: the
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// and never provision) provision system/known branch names, not
|
|
105
|
-
// user-chosen ones, and deliberately stay uncovered -- intentional, not
|
|
106
|
-
// an oversight.
|
|
115
|
+
// Scope note: the remote-mirror check further down applies ONLY to this
|
|
116
|
+
// user-facing creation path; http/handler.ts's auto-create (base/active
|
|
117
|
+
// branches) and loadOrCreateBranchContext provision known names. The two
|
|
118
|
+
// settings-branch checks below give this path a specific 400; every
|
|
119
|
+
// provisioning path, this one included, also refuses a settings branch in
|
|
120
|
+
// BranchWorkspaceManager.openOrCreateBranch.
|
|
107
121
|
// Prevent git branch name collision with the settings branch. Settings
|
|
108
122
|
// live in a separate directory but share the same git remote, and
|
|
109
123
|
// openOrCreateBranch (branch-workspace.ts) uses the SANITIZED name as the
|
|
@@ -154,13 +168,9 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
154
168
|
error: `Branch name "${branchName}" is reserved: it collides with the /${branchName} API route namespace`,
|
|
155
169
|
};
|
|
156
170
|
}
|
|
157
|
-
// Reject the base branch name outright
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// naming the base branch would let the caller inject themselves into the
|
|
161
|
-
// protected base branch's ACL (gaining e.g. withdraw rights via the
|
|
162
|
-
// allowed_by_acl path). No recorded fork point exists yet for a
|
|
163
|
-
// not-yet-created branch, so this checks config protection only.
|
|
171
|
+
// Reject the base branch name outright, even before the base branch is
|
|
172
|
+
// provisioned. No recorded fork point exists yet for a not-yet-created
|
|
173
|
+
// branch, so this checks config protection only.
|
|
164
174
|
const { isProtected } = getBranchProtection(ctx.services.config, branchName);
|
|
165
175
|
if (isProtected) {
|
|
166
176
|
return {
|
|
@@ -169,14 +179,10 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
169
179
|
error: 'Cannot create a branch with the base branch name',
|
|
170
180
|
};
|
|
171
181
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// since that's what's persisted in branch.json (see
|
|
177
|
-
// BranchWorkspaceManager.openOrCreateBranch). System branches
|
|
178
|
-
// auto-provisioned via http/handler.ts's getBranchContext don't go
|
|
179
|
-
// through this handler, so rejecting collisions here doesn't affect them.
|
|
182
|
+
// A name collision with ANY existing branch answers before a clone is paid
|
|
183
|
+
// for. Provisioning never merges into an existing branch either: it reports
|
|
184
|
+
// `exists`, mapped the same way below. Compared sanitized, the form
|
|
185
|
+
// branch.json records.
|
|
180
186
|
if (!ctx.services.registry) {
|
|
181
187
|
return {
|
|
182
188
|
ok: false,
|
|
@@ -186,11 +192,7 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
186
192
|
}
|
|
187
193
|
const existingBranch = await ctx.services.registry.get(sanitizeBranchName(branchName));
|
|
188
194
|
if (existingBranch) {
|
|
189
|
-
return
|
|
190
|
-
ok: false,
|
|
191
|
-
status: 409,
|
|
192
|
-
error: 'A branch with this name already exists',
|
|
193
|
-
};
|
|
195
|
+
return existingBranchResponse(ctx, req, existingBranch);
|
|
194
196
|
}
|
|
195
197
|
// L2: create-time collision check against this deployment's local
|
|
196
198
|
// GitHub mirror (`remote.git`). The CMS Lambda has no internet access
|
|
@@ -315,19 +317,47 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
315
317
|
};
|
|
316
318
|
}
|
|
317
319
|
const manager = new BranchWorkspaceManager(ctx.services.config);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
320
|
+
let outcome;
|
|
321
|
+
try {
|
|
322
|
+
outcome = await manager.provisionBranch({
|
|
323
|
+
branchName,
|
|
324
|
+
mode: ctx.services.config.mode,
|
|
325
|
+
createdBy: req.user.userId,
|
|
326
|
+
title: body.title,
|
|
327
|
+
description: body.description,
|
|
328
|
+
access: body.access,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (err instanceof BranchProvisioningBusyError) {
|
|
333
|
+
return { ok: false, status: 503, error: err.message };
|
|
334
|
+
}
|
|
335
|
+
if (err instanceof BranchDirOccupiedError) {
|
|
336
|
+
return { ok: false, status: 409, error: err.message };
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
if (outcome.kind === 'exists')
|
|
341
|
+
return existingBranchResponse(ctx, req, outcome.context);
|
|
342
|
+
log.debug('api', 'Branch created', { branchName: outcome.context.branch.name });
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
status: 200,
|
|
346
|
+
data: { branch: toBranchListItem(ctx.services.config, outcome.context.branch) },
|
|
347
|
+
};
|
|
329
348
|
});
|
|
330
349
|
};
|
|
350
|
+
/** A create that names an existing branch: 409, unless it is that creator's recent retry. */
|
|
351
|
+
const existingBranchResponse = (ctx, req, existing) => {
|
|
352
|
+
if (isCreatorsRecentBranch(existing.branch, req.user.userId)) {
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
status: 200,
|
|
356
|
+
data: { branch: toBranchListItem(ctx.services.config, existing.branch) },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { ok: false, status: 409, error: 'A branch with this name already exists' };
|
|
360
|
+
};
|
|
331
361
|
/** @internal Exported for tests. */
|
|
332
362
|
export const listBranchesHandler = async (ctx, req) => {
|
|
333
363
|
if (!ctx.services.registry) {
|
|
@@ -337,7 +367,10 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
337
367
|
error: 'Branch registry not initialized — ensure the workspace has been initialized',
|
|
338
368
|
};
|
|
339
369
|
}
|
|
340
|
-
|
|
370
|
+
// A settings-branch workspace is never resolvable as a content branch (see
|
|
371
|
+
// http/handler.ts), so one left on disk is hidden rather than listed unopenable.
|
|
372
|
+
const settingsBranch = operatingStrategy(ctx.services.config.mode).getSettingsBranchName(ctx.services.config);
|
|
373
|
+
const allBranches = (await ctx.services.registry.list()).filter((context) => !isSettingsBranchName(context.branch.name, settingsBranch));
|
|
341
374
|
// The branch the editor should open when none is pinned via URL/config.
|
|
342
375
|
// Read per-request so dev-mode refreshActiveBranch() updates are reflected.
|
|
343
376
|
// Sanitized: dev mode detects the RAW git HEAD name (e.g. 'claude/foo'),
|
|
@@ -345,24 +378,15 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
345
378
|
// editor matches defaultBranch against registry names, so return the form
|
|
346
379
|
// that can actually be found there.
|
|
347
380
|
const defaultBranch = sanitizeBranchName(ctx.services.config.defaultActiveBranch ?? ctx.services.config.defaultBaseBranch ?? 'main');
|
|
348
|
-
// Attach server-computed protected-base-branch flags; read config per-request
|
|
349
|
-
// so dev-mode refreshActiveBranch() updates are reflected here too.
|
|
350
|
-
const toListItem = (context) => {
|
|
351
|
-
const protection = getBranchWriteProtection(ctx.services.config, context.branch.name, context.branch.baseBranch, context.branch.status);
|
|
352
|
-
return {
|
|
353
|
-
...context.branch,
|
|
354
|
-
isProtected: protection.isProtected,
|
|
355
|
-
readOnly: protection.readOnly,
|
|
356
|
-
writeBlocked: protection.writeBlocked,
|
|
357
|
-
submitBlocked: protection.submitBlockedIncludingStatus,
|
|
358
|
-
};
|
|
359
|
-
};
|
|
360
381
|
// Admins and Reviewers see all branches
|
|
361
382
|
if (isPrivileged(req.user.groups)) {
|
|
362
383
|
return {
|
|
363
384
|
ok: true,
|
|
364
385
|
status: 200,
|
|
365
|
-
data: {
|
|
386
|
+
data: {
|
|
387
|
+
branches: allBranches.map((context) => toBranchListItem(ctx.services.config, context.branch)),
|
|
388
|
+
defaultBranch,
|
|
389
|
+
},
|
|
366
390
|
};
|
|
367
391
|
}
|
|
368
392
|
// Regular users only see branches they created or have explicit access to
|
|
@@ -390,7 +414,10 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
390
414
|
return {
|
|
391
415
|
ok: true,
|
|
392
416
|
status: 200,
|
|
393
|
-
data: {
|
|
417
|
+
data: {
|
|
418
|
+
branches: visibleBranches.map((context) => toBranchListItem(ctx.services.config, context.branch)),
|
|
419
|
+
defaultBranch,
|
|
420
|
+
},
|
|
394
421
|
};
|
|
395
422
|
};
|
|
396
423
|
/** @internal Exported for tests. */
|
|
@@ -456,48 +483,39 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
456
483
|
error: 'Cannot delete branch with open pull request',
|
|
457
484
|
};
|
|
458
485
|
}
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
486
|
+
// The branch directory leaves its name by one rename, under the same
|
|
487
|
+
// server-enforced lockfile branch-metadata saves hold (see
|
|
488
|
+
// utils/occ-json-write.ts), so a save() that reaches that lock after the
|
|
489
|
+
// rename fails rather than recreating the tree, and a process killed during
|
|
490
|
+
// the `rm` leaves
|
|
491
|
+
// only a `.deleting-*` directory the worker sweeps, never residue under a
|
|
492
|
+
// name that may be created again.
|
|
466
493
|
const metadataFile = path.join(branchContext.branchRoot, '.canopy-meta', 'branch.json');
|
|
467
494
|
let cleanupWarning;
|
|
495
|
+
let deletingPath;
|
|
468
496
|
try {
|
|
469
497
|
await withOccFileLock(metadataFile, async () => {
|
|
498
|
+
if (branchContext.branchRoot === branchContext.baseRoot) {
|
|
499
|
+
await fs.unlink(metadataFile).catch((err) => {
|
|
500
|
+
if (!isNotFoundError(err))
|
|
501
|
+
throw err;
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const target = path.join(branchContext.baseRoot, deletingDirName(path.basename(branchContext.branchRoot)));
|
|
470
506
|
try {
|
|
471
|
-
await fs.
|
|
507
|
+
await fs.rename(branchContext.branchRoot, target);
|
|
508
|
+
deletingPath = target;
|
|
472
509
|
}
|
|
473
510
|
catch (err) {
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// deleted and will no longer appear in listings), but silently
|
|
483
|
-
// succeeding here would leave a full orphan clone on disk with
|
|
484
|
-
// nothing in the API surfacing its existence.
|
|
485
|
-
if (branchContext.branchRoot !== branchContext.baseRoot) {
|
|
486
|
-
try {
|
|
487
|
-
await fs.rm(branchContext.branchRoot, {
|
|
488
|
-
recursive: true,
|
|
489
|
-
force: true,
|
|
490
|
-
maxRetries: 3,
|
|
491
|
-
retryDelay: 100,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
// sanitizeErrorMessage: this string goes back to the browser in the
|
|
496
|
-
// delete response (API-H2 — no absolute EFS paths to clients);
|
|
497
|
-
// the console line below keeps the raw detail for server logs.
|
|
498
|
-
cleanupWarning = `Failed to fully remove branch directory: ${sanitizeErrorMessage(getErrorMessage(err))}`;
|
|
499
|
-
console.error(`CanopyCMS: Failed to delete branch directory for ${branchName}:`, getErrorMessage(err));
|
|
500
|
-
}
|
|
511
|
+
if (isNotFoundError(err))
|
|
512
|
+
return;
|
|
513
|
+
// Still deleted logically: without branch.json the branch is unlisted,
|
|
514
|
+
// and the worker quarantines what is left.
|
|
515
|
+
await fs.unlink(metadataFile).catch(() => { });
|
|
516
|
+
// [REDACT] Returned to the browser; the console line keeps the path.
|
|
517
|
+
cleanupWarning = `Failed to remove branch directory: ${sanitizeErrorMessage(getErrorMessage(err))}`;
|
|
518
|
+
console.error(`CanopyCMS: Failed to move branch directory for ${branchName} aside:`, getErrorMessage(err));
|
|
501
519
|
}
|
|
502
520
|
});
|
|
503
521
|
}
|
|
@@ -510,6 +528,8 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
510
528
|
error: `Branch is busy, try again: ${getErrorMessage(err)}`,
|
|
511
529
|
};
|
|
512
530
|
}
|
|
531
|
+
if (deletingPath)
|
|
532
|
+
await removeLeftoverDir(deletingPath);
|
|
513
533
|
// Also delete the branch's local head from the remote.git mirror (the
|
|
514
534
|
// deployment's local origin). The sync loop deliberately never deletes a
|
|
515
535
|
// local head (see reconcileTrackedBranches), so THIS is the one explicit
|
|
@@ -549,6 +569,12 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
549
569
|
console.warn(`CanopyCMS: failed to remove deleted branch's mirror head (branch ${sanitizedDeleted}, mirror ${mirrorPath}): ${getErrorMessage(err)}`);
|
|
550
570
|
}
|
|
551
571
|
}
|
|
572
|
+
// `branchContext` was read before the delete, so it still carries the PR number this gates on.
|
|
573
|
+
if (deletableHead) {
|
|
574
|
+
const warning = await syncDeleteRemoteBranch(ctx, branchContext);
|
|
575
|
+
if (warning)
|
|
576
|
+
cleanupWarning = cleanupWarning ? `${cleanupWarning}; ${warning}` : warning;
|
|
577
|
+
}
|
|
552
578
|
// Invalidate registry cache so next list() will regenerate without this branch
|
|
553
579
|
if (!ctx.services.registry) {
|
|
554
580
|
return {
|
|
@@ -646,7 +672,7 @@ const createBranch = defineEndpoint({
|
|
|
646
672
|
path: '/branches',
|
|
647
673
|
body: createBranchBodySchema,
|
|
648
674
|
bodyType: 'CreateBranchBody',
|
|
649
|
-
responseType: '
|
|
675
|
+
responseType: 'BranchListItemResponse',
|
|
650
676
|
response: {},
|
|
651
677
|
defaultMockData: {
|
|
652
678
|
branch: {
|