canopycms 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -47
- package/dist/api/admin-branch-health.js +47 -20
- package/dist/api/admin.d.ts +13 -1
- package/dist/api/admin.js +14 -24
- package/dist/api/branch-changes.d.ts +87 -0
- package/dist/api/branch-changes.js +269 -0
- package/dist/api/branch-entry-changes.d.ts +83 -0
- package/dist/api/branch-entry-changes.js +262 -0
- package/dist/api/branch-merge.js +2 -2
- package/dist/api/branch-review.js +35 -4
- package/dist/api/branch-status.js +14 -5
- package/dist/api/branch-withdraw.js +7 -0
- package/dist/api/branch.d.ts +13 -1
- package/dist/api/branch.js +171 -97
- package/dist/api/client.d.ts +15 -0
- package/dist/api/client.js +13 -1
- package/dist/api/content.d.ts +18 -18
- package/dist/api/content.js +1 -1
- package/dist/api/github-sync.js +7 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/pr-comment-sync.d.ts +9 -8
- package/dist/api/pr-comment-sync.js +23 -13
- package/dist/api/routes.js +4 -0
- package/dist/api/types.d.ts +3 -1
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/protected-branch.d.ts +3 -3
- package/dist/authorization/protected-branch.js +3 -3
- package/dist/branch-metadata-file.d.ts +84 -0
- package/dist/branch-metadata-file.js +9 -0
- package/dist/branch-metadata.d.ts +4 -4
- package/dist/branch-metadata.js +4 -5
- package/dist/branch-param.d.ts +24 -0
- package/dist/branch-param.js +87 -0
- package/dist/branch-provisioning.d.ts +6 -3
- package/dist/branch-provisioning.js +12 -10
- package/dist/branch-registry.d.ts +6 -0
- package/dist/branch-registry.js +12 -3
- package/dist/branch-workspace.js +22 -15
- package/dist/cli/cli.js +1060 -172
- package/dist/cli/generate-ai-content.js +732 -101
- package/dist/cli/init.js +126 -0
- package/dist/comment-store.d.ts +3 -0
- package/dist/comment-store.js +4 -1
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/types.d.ts +11 -0
- package/dist/content-id-index.d.ts +5 -0
- package/dist/content-id-index.js +1 -1
- package/dist/content-listing.d.ts +53 -0
- package/dist/content-listing.js +123 -27
- package/dist/content-reader.js +23 -16
- package/dist/context.js +8 -7
- package/dist/editor/BranchManager.d.ts +7 -3
- package/dist/editor/BranchManager.js +51 -6
- package/dist/editor/Editor.js +166 -34
- package/dist/editor/EntryNavigator.d.ts +34 -10
- package/dist/editor/EntryNavigator.js +185 -29
- package/dist/editor/admin/SystemHealthPanel.js +36 -1
- package/dist/editor/admin/useSystemHealth.js +1 -0
- package/dist/editor/branch-names.d.ts +43 -0
- package/dist/editor/branch-names.js +75 -0
- package/dist/editor/components/BranchPill.d.ts +2 -2
- package/dist/editor/components/BranchPill.js +2 -2
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +2 -2
- package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
- package/dist/editor/components/EntryChangesDrawer.js +363 -0
- package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
- package/dist/editor/components/EntryChangesSkeleton.js +7 -0
- package/dist/editor/components/EntryHeader.d.ts +9 -1
- package/dist/editor/components/EntryHeader.js +25 -3
- package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
- package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
- package/dist/editor/components/ReviewDialogs.js +3 -3
- package/dist/editor/context/ApiClientContext.d.ts +9 -0
- package/dist/editor/context/ApiClientContext.js +26 -2
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/copy.d.ts +1 -0
- package/dist/editor/copy.js +1 -0
- package/dist/editor/editor-utils.d.ts +2 -0
- package/dist/editor/editor-utils.js +15 -0
- package/dist/editor/entry-diff.d.ts +143 -0
- package/dist/editor/entry-diff.js +593 -0
- package/dist/editor/hooks/create-branch-request.d.ts +11 -1
- package/dist/editor/hooks/create-branch-request.js +16 -6
- package/dist/editor/hooks/useBranchActions.d.ts +19 -2
- package/dist/editor/hooks/useBranchActions.js +29 -20
- package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchChangesData.js +134 -0
- package/dist/editor/hooks/useBranchManager.d.ts +16 -0
- package/dist/editor/hooks/useBranchManager.js +142 -54
- package/dist/editor/hooks/useDraftManager.js +6 -2
- package/dist/editor/hooks/useEntryManager.d.ts +18 -2
- package/dist/editor/hooks/useEntryManager.js +36 -10
- package/dist/editor/utils/editor-errors.d.ts +5 -2
- package/dist/git-manager.d.ts +71 -0
- package/dist/git-manager.js +270 -1
- package/dist/http/handler.js +11 -9
- package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
- package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
- package/dist/operating-mode/branch-namespace.d.ts +34 -0
- package/dist/operating-mode/branch-namespace.js +121 -0
- package/dist/operating-mode/client-unsafe-strategy.js +7 -0
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/paths/branch-name.d.ts +11 -19
- package/dist/paths/branch-name.js +17 -27
- package/dist/paths/index.d.ts +2 -2
- package/dist/paths/index.js +2 -2
- package/dist/paths/resolve.d.ts +0 -1
- package/dist/paths/resolve.js +0 -1
- package/dist/paths/validation.d.ts +40 -1
- package/dist/paths/validation.js +83 -4
- package/dist/pr-comments.d.ts +17 -9
- package/dist/pr-comments.js +53 -15
- package/dist/services.js +2 -1
- package/dist/submission-attribution.d.ts +22 -0
- package/dist/submission-attribution.js +62 -6
- package/dist/task-queue/worker-status.d.ts +6 -1
- package/dist/task-queue/worker-status.js +23 -2
- package/dist/types.d.ts +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +111 -16
- package/dist/worker/branch-policy.js +222 -17
- package/dist/worker/cms-worker.d.ts +24 -2
- package/dist/worker/cms-worker.js +35 -24
- package/dist/worker/git-sync.d.ts +3 -3
- package/dist/worker/git-sync.js +47 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway-wire.d.ts +40 -25
- package/dist/worker/github-gateway-wire.js +14 -3
- package/dist/worker/github-gateway.d.ts +17 -6
- package/dist/worker/github-gateway.js +62 -6
- package/dist/worker/github-mirror.d.ts +7 -4
- package/dist/worker/github-mirror.js +27 -16
- package/dist/worker/history-rewrite.d.ts +23 -10
- package/dist/worker/history-rewrite.js +43 -20
- package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
- package/dist/worker/legacy-branch-fixtures.js +71 -0
- package/dist/worker/rebase.d.ts +2 -1
- package/dist/worker/rebase.js +93 -36
- package/dist/worker/task-runner.d.ts +4 -1
- package/dist/worker/task-runner.js +77 -48
- package/dist/worker/unpublishable-branches.d.ts +34 -0
- package/dist/worker/unpublishable-branches.js +182 -0
- package/package.json +2 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single resolution point for the branch namespace: the prefix every branch the GitHub gateway
|
|
3
|
+
* writes starts with, apart from this deployment's settings branch and the adopter's
|
|
4
|
+
* `legacyBranches` (worker/branch-policy.ts). It comes from a template in which `{deployment}`
|
|
5
|
+
* stands for the deployment name, so two deployments sharing a repository each get their own.
|
|
6
|
+
*
|
|
7
|
+
* Precedence, as for the deployment name and for the same reason: `CANOPYCMS_BRANCH_NAMESPACE`,
|
|
8
|
+
* which infrastructure stamps per stack, then `config.branchNamespace`, then
|
|
9
|
+
* {@link DEFAULT_BRANCH_NAMESPACE_TEMPLATE}. The gateway resolves its own from its own
|
|
10
|
+
* environment, never from anything the CMS Lambda can write.
|
|
11
|
+
*/
|
|
12
|
+
import { RESERVED_SETTINGS_BRANCH_PREFIX, sanitizeBranchName } from '../paths/branch-name.js';
|
|
13
|
+
import { parseBranchRef } from '../paths/validation.js';
|
|
14
|
+
import { canopyLogWarn } from '../utils/logger.js';
|
|
15
|
+
import { resolveDeploymentName } from './deployment-name.js';
|
|
16
|
+
let warned = false;
|
|
17
|
+
function warnOnce(message) {
|
|
18
|
+
if (warned)
|
|
19
|
+
return;
|
|
20
|
+
warned = true;
|
|
21
|
+
canopyLogWarn(message);
|
|
22
|
+
}
|
|
23
|
+
/** The one placeholder a template may hold. */
|
|
24
|
+
const DEPLOYMENT_PLACEHOLDER = '{deployment}';
|
|
25
|
+
/** The template used when none is configured. */
|
|
26
|
+
export const DEFAULT_BRANCH_NAMESPACE_TEMPLATE = `canopycms/${DEPLOYMENT_PLACEHOLDER}/`;
|
|
27
|
+
/**
|
|
28
|
+
* The environment variable that carries the template, or the namespace it resolved to.
|
|
29
|
+
* @internal Exported for tests.
|
|
30
|
+
*/
|
|
31
|
+
export const BRANCH_NAMESPACE_ENV = 'CANOPYCMS_BRANCH_NAMESPACE';
|
|
32
|
+
/**
|
|
33
|
+
* Why `namespace` cannot be a branch namespace, or null. It ends with `/`: a deployment name holds
|
|
34
|
+
* no `/`, so `canopycms/prod/` never starts `canopycms/prod-eu/`, as `canopycms-prod-` would start
|
|
35
|
+
* `canopycms-prod-eu-`. A branch in it must be a name git and the gateway take (checked with a
|
|
36
|
+
* branch appended), and none may be a settings branch, by name or by workspace directory.
|
|
37
|
+
*/
|
|
38
|
+
export function branchNamespaceProblem(namespace) {
|
|
39
|
+
if (!namespace.endsWith('/'))
|
|
40
|
+
return 'it must end with "/"';
|
|
41
|
+
const sample = `${namespace}x`;
|
|
42
|
+
if (!/^[\x21-\x7e]+$/.test(namespace))
|
|
43
|
+
return 'it is not printable ASCII without spaces';
|
|
44
|
+
if (/["%{}]/.test(namespace))
|
|
45
|
+
return 'it contains ", %, { or }';
|
|
46
|
+
if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
|
|
47
|
+
return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
|
|
48
|
+
}
|
|
49
|
+
const parsed = parseBranchRef(sample);
|
|
50
|
+
if (!parsed.ok)
|
|
51
|
+
return `a branch in it would be refused (${parsed.error})`;
|
|
52
|
+
if (sample.split('/').some((part) => part.startsWith('.') || part.endsWith('.lock'))) {
|
|
53
|
+
return "a part of it starts with '.' or ends with '.lock'";
|
|
54
|
+
}
|
|
55
|
+
// A branch's directory is the namespace's own sanitized form followed by the rest, so a
|
|
56
|
+
// namespace that sanitizes to a start of the settings prefix (`canopycms/`) holds settings names.
|
|
57
|
+
const directoryPrefix = sanitizeBranchName(namespace);
|
|
58
|
+
if (RESERVED_SETTINGS_BRANCH_PREFIX.startsWith(directoryPrefix) ||
|
|
59
|
+
sanitizeBranchName(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
|
|
60
|
+
return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX}* settings branches`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The branch names git cannot hold beside any branch in `namespace`: each path it runs through
|
|
66
|
+
* (`canopycms` and `canopycms/prod` for `canopycms/prod/`), since no ref lives under a branch.
|
|
67
|
+
*/
|
|
68
|
+
export function namespaceAncestors(namespace) {
|
|
69
|
+
const parts = namespace.split('/').filter(Boolean);
|
|
70
|
+
return parts.map((_, i) => parts.slice(0, i + 1).join('/'));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The namespace `template` names for `deploymentName`, or why it cannot be one. Throws, naming
|
|
74
|
+
* `source`, for a template with a placeholder other than `{deployment}` or a namespace
|
|
75
|
+
* {@link branchNamespaceProblem} refuses.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveBranchNamespaceTemplate(template, deploymentName, source) {
|
|
78
|
+
const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER, '').match(/\{[^}]*\}?/);
|
|
79
|
+
if (unknown) {
|
|
80
|
+
throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(template)} (from ${source}): ` +
|
|
81
|
+
`${JSON.stringify(unknown[0])} is not a placeholder; the only one is ${DEPLOYMENT_PLACEHOLDER}`);
|
|
82
|
+
}
|
|
83
|
+
const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER, deploymentName);
|
|
84
|
+
const problem = branchNamespaceProblem(namespace);
|
|
85
|
+
if (problem !== null) {
|
|
86
|
+
throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
|
|
87
|
+
}
|
|
88
|
+
return namespace;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The branch namespace a process resolves: from `env`'s `CANOPYCMS_BRANCH_NAMESPACE` (trimmed,
|
|
92
|
+
* empty ignored), then `config.branchNamespace`, then the default, with `{deployment}` filled by
|
|
93
|
+
* `resolveDeploymentName`. Throws for an invalid template or deployment name.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveBranchNamespace(config, modeDefault, env = process.env) {
|
|
96
|
+
const fromEnv = env[BRANCH_NAMESPACE_ENV]?.trim();
|
|
97
|
+
const [template, source] = fromEnv
|
|
98
|
+
? [fromEnv, BRANCH_NAMESPACE_ENV]
|
|
99
|
+
: config.branchNamespace !== undefined
|
|
100
|
+
? [config.branchNamespace, 'config.branchNamespace']
|
|
101
|
+
: [DEFAULT_BRANCH_NAMESPACE_TEMPLATE, 'the default'];
|
|
102
|
+
const deploymentName = resolveDeploymentName({ deploymentName: config.deploymentName }, modeDefault);
|
|
103
|
+
const namespace = resolveBranchNamespaceTemplate(template, deploymentName, source);
|
|
104
|
+
// Compared resolved: infrastructure stamps the namespace, config holds the template.
|
|
105
|
+
if (fromEnv && config.branchNamespace !== undefined) {
|
|
106
|
+
let fromConfig = null;
|
|
107
|
+
try {
|
|
108
|
+
fromConfig = resolveBranchNamespaceTemplate(config.branchNamespace, deploymentName, 'config.branchNamespace');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// An invalid config template is warned about too: the environment variable wins anyway.
|
|
112
|
+
}
|
|
113
|
+
if (fromConfig !== namespace) {
|
|
114
|
+
warnOnce(`CanopyCMS: ${BRANCH_NAMESPACE_ENV} (${JSON.stringify(fromEnv)}) and ` +
|
|
115
|
+
`config.branchNamespace (${JSON.stringify(config.branchNamespace)}) do not resolve to ` +
|
|
116
|
+
'the same valid namespace; using the environment variable, which infrastructure stamps ' +
|
|
117
|
+
'per stack.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return namespace;
|
|
121
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { ProdClientSafeStrategy, DevClientSafeStrategy } from './client-safe-strategy.js';
|
|
7
|
+
import { resolveBranchNamespace } from './branch-namespace.js';
|
|
7
8
|
import { resolveDeploymentName } from './deployment-name.js';
|
|
8
9
|
import { DEFAULT_PROD_WORKSPACE } from '../config/index.js';
|
|
9
10
|
class ProdStrategy extends ProdClientSafeStrategy {
|
|
@@ -46,6 +47,9 @@ class ProdStrategy extends ProdClientSafeStrategy {
|
|
|
46
47
|
return config.settingsBranch;
|
|
47
48
|
return `canopycms-settings-${resolveDeploymentName(config, 'prod')}`;
|
|
48
49
|
}
|
|
50
|
+
getBranchNamespace(config) {
|
|
51
|
+
return resolveBranchNamespace(config, 'prod');
|
|
52
|
+
}
|
|
49
53
|
getSettingsRoot(sourceRoot) {
|
|
50
54
|
return path.join(this.getWorkspaceRoot(sourceRoot), 'settings');
|
|
51
55
|
}
|
|
@@ -95,6 +99,9 @@ class DevStrategy extends DevClientSafeStrategy {
|
|
|
95
99
|
return config.settingsBranch;
|
|
96
100
|
return `canopycms-settings-${resolveDeploymentName(config, 'local')}`;
|
|
97
101
|
}
|
|
102
|
+
getBranchNamespace(config) {
|
|
103
|
+
return resolveBranchNamespace(config, 'local');
|
|
104
|
+
}
|
|
98
105
|
getSettingsRoot(sourceRoot) {
|
|
99
106
|
return path.join(this.getWorkspaceRoot(sourceRoot), 'settings');
|
|
100
107
|
}
|
|
@@ -82,6 +82,14 @@ export interface ClientUnsafeStrategy extends ClientSafeStrategy {
|
|
|
82
82
|
deploymentName?: string;
|
|
83
83
|
defaultBaseBranch?: string;
|
|
84
84
|
}): string;
|
|
85
|
+
/**
|
|
86
|
+
* The prefix of every branch editors create, resolved from the namespace template with this
|
|
87
|
+
* mode's default deployment name (operating-mode/branch-namespace.ts).
|
|
88
|
+
*/
|
|
89
|
+
getBranchNamespace(config: {
|
|
90
|
+
branchNamespace?: string;
|
|
91
|
+
deploymentName?: string;
|
|
92
|
+
}): string;
|
|
85
93
|
/** `{workspaceRoot}/settings`, where settings storage lives. */
|
|
86
94
|
getSettingsRoot(sourceRoot?: string): string;
|
|
87
95
|
usesSeparateSettingsBranch(): boolean;
|
|
@@ -18,9 +18,9 @@ export declare function sanitizeBranchName(branchName: string): SanitizedBranchN
|
|
|
18
18
|
* Exported from this dependency-free module -- not rebuilt ad hoc at each call
|
|
19
19
|
* site -- so every consumer of the namespace agrees on the exact string:
|
|
20
20
|
* worker/git-sync.ts's `pushSettingsBranches` never pushes a
|
|
21
|
-
* `canopycms-settings-*` branch this deployment doesn't own, and
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* `canopycms-settings-*` branch this deployment doesn't own, and
|
|
22
|
+
* operating-mode/branch-namespace.ts refuses a branch namespace whose branches
|
|
23
|
+
* could fall in it.
|
|
24
24
|
*
|
|
25
25
|
* The WHOLE prefix is reserved, not just this deployment's own settings branch:
|
|
26
26
|
* two deployments can share one GitHub repo, and a content branch landing on
|
|
@@ -29,24 +29,16 @@ export declare function sanitizeBranchName(branchName: string): SanitizedBranchN
|
|
|
29
29
|
* branch.
|
|
30
30
|
*/
|
|
31
31
|
export declare const RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
32
|
+
/**
|
|
33
|
+
* Branch `name`'s short name: the part after `namespace` for a branch in it, the full name for
|
|
34
|
+
* any other, and for one whose remainder holds a `/`. So a short name holding a `/` is always a
|
|
35
|
+
* full name, which the editor's unlisted request segment (editor/branch-names.ts `unlistedSegment`)
|
|
36
|
+
* relies on. The server rebuilds the ref from it (branch-param.ts's `resolveBranchParam`); the
|
|
37
|
+
* editor is never told the namespace.
|
|
38
|
+
*/
|
|
39
|
+
export declare function branchShortName(name: string, namespace: string | null): string;
|
|
32
40
|
/**
|
|
33
41
|
* Whether `branchName` is a settings branch, never a content branch: the reserved prefix, or the
|
|
34
42
|
* configured name (which need not carry it). Compared sanitized, the form git and disk use.
|
|
35
43
|
*/
|
|
36
44
|
export declare function isSettingsBranchName(branchName: string, settingsBranchName?: string): boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Branch names that collide with a static top-level API route namespace.
|
|
39
|
-
*
|
|
40
|
-
* http/router.ts's `compareSpecificity` ranks a literal pattern segment above a
|
|
41
|
-
* `:param`, so a branch named e.g. `admin` has its `/:branch/...` routes
|
|
42
|
-
* shadowed by the static `/admin/...` ones -- and only partially, which is what
|
|
43
|
-
* makes it confusing: bare `GET /admin` still reaches the branch handler, so the
|
|
44
|
-
* branch looks half-alive while every nested route 404s or 403s.
|
|
45
|
-
*
|
|
46
|
-
* Listed rather than derived at runtime because api/validators.ts is imported
|
|
47
|
-
* *by* the route modules, so importing the router here would cycle.
|
|
48
|
-
* http/router.test.ts derives the same set from the live route table and asserts
|
|
49
|
-
* it equals this constant, so a new top-level namespace fails that test until
|
|
50
|
-
* this list is updated.
|
|
51
|
-
*/
|
|
52
|
-
export declare const RESERVED_ROUTE_BRANCH_NAMES: readonly string[];
|
|
@@ -22,9 +22,9 @@ export function sanitizeBranchName(branchName) {
|
|
|
22
22
|
* Exported from this dependency-free module -- not rebuilt ad hoc at each call
|
|
23
23
|
* site -- so every consumer of the namespace agrees on the exact string:
|
|
24
24
|
* worker/git-sync.ts's `pushSettingsBranches` never pushes a
|
|
25
|
-
* `canopycms-settings-*` branch this deployment doesn't own, and
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* `canopycms-settings-*` branch this deployment doesn't own, and
|
|
26
|
+
* operating-mode/branch-namespace.ts refuses a branch namespace whose branches
|
|
27
|
+
* could fall in it.
|
|
28
28
|
*
|
|
29
29
|
* The WHOLE prefix is reserved, not just this deployment's own settings branch:
|
|
30
30
|
* two deployments can share one GitHub repo, and a content branch landing on
|
|
@@ -33,6 +33,20 @@ export function sanitizeBranchName(branchName) {
|
|
|
33
33
|
* branch.
|
|
34
34
|
*/
|
|
35
35
|
export const RESERVED_SETTINGS_BRANCH_PREFIX = 'canopycms-settings-';
|
|
36
|
+
/**
|
|
37
|
+
* Branch `name`'s short name: the part after `namespace` for a branch in it, the full name for
|
|
38
|
+
* any other, and for one whose remainder holds a `/`. So a short name holding a `/` is always a
|
|
39
|
+
* full name, which the editor's unlisted request segment (editor/branch-names.ts `unlistedSegment`)
|
|
40
|
+
* relies on. The server rebuilds the ref from it (branch-param.ts's `resolveBranchParam`); the
|
|
41
|
+
* editor is never told the namespace.
|
|
42
|
+
*/
|
|
43
|
+
export function branchShortName(name, namespace) {
|
|
44
|
+
if (namespace === null || !name.startsWith(namespace) || name.length === namespace.length) {
|
|
45
|
+
return name;
|
|
46
|
+
}
|
|
47
|
+
const rest = name.slice(namespace.length);
|
|
48
|
+
return rest.includes('/') ? name : rest;
|
|
49
|
+
}
|
|
36
50
|
/**
|
|
37
51
|
* Whether `branchName` is a settings branch, never a content branch: the reserved prefix, or the
|
|
38
52
|
* configured name (which need not carry it). Compared sanitized, the form git and disk use.
|
|
@@ -42,27 +56,3 @@ export function isSettingsBranchName(branchName, settingsBranchName) {
|
|
|
42
56
|
return (sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) ||
|
|
43
57
|
(settingsBranchName !== undefined && sanitized === sanitizeBranchName(settingsBranchName)));
|
|
44
58
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Branch names that collide with a static top-level API route namespace.
|
|
47
|
-
*
|
|
48
|
-
* http/router.ts's `compareSpecificity` ranks a literal pattern segment above a
|
|
49
|
-
* `:param`, so a branch named e.g. `admin` has its `/:branch/...` routes
|
|
50
|
-
* shadowed by the static `/admin/...` ones -- and only partially, which is what
|
|
51
|
-
* makes it confusing: bare `GET /admin` still reaches the branch handler, so the
|
|
52
|
-
* branch looks half-alive while every nested route 404s or 403s.
|
|
53
|
-
*
|
|
54
|
-
* Listed rather than derived at runtime because api/validators.ts is imported
|
|
55
|
-
* *by* the route modules, so importing the router here would cycle.
|
|
56
|
-
* http/router.test.ts derives the same set from the live route table and asserts
|
|
57
|
-
* it equals this constant, so a new top-level namespace fails that test until
|
|
58
|
-
* this list is updated.
|
|
59
|
-
*/
|
|
60
|
-
export const RESERVED_ROUTE_BRANCH_NAMES = [
|
|
61
|
-
'admin',
|
|
62
|
-
'assets',
|
|
63
|
-
'branches',
|
|
64
|
-
'groups',
|
|
65
|
-
'permissions',
|
|
66
|
-
'users',
|
|
67
|
-
'whoami',
|
|
68
|
-
];
|
package/dist/paths/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export type { LogicalPath, PhysicalPath, SanitizedBranchName, BranchName, ContentId, Slug, PathValidationResult, } from './types.js';
|
|
6
6
|
export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, entryLogicalPath, joinPath, trimSlashes, } from './normalize.js';
|
|
7
7
|
export { validateAndNormalizePath } from './normalize-server.js';
|
|
8
|
-
export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseSlug, } from './validation.js';
|
|
8
|
+
export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseBranchRef, parseCreatedBranchName, parseSlug, } from './validation.js';
|
|
9
9
|
export { resolveLogicalPath } from './resolve.js';
|
|
10
10
|
export { resolveBranchPath, getDefaultBranchBase, resolveBranchPaths, BranchPathError, namesNoWorkspace, type BranchPathOptions, type BranchPathResult, } from './branch.js';
|
|
11
|
-
export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX,
|
|
11
|
+
export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, branchShortName, isSettingsBranchName, } from './branch-name.js';
|
package/dist/paths/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export { normalizeFilesystemPath, normalizeCollectionPath, hasTraversalSequence, createLogicalPath, createPhysicalPath, entryLogicalPath, joinPath, trimSlashes, } from './normalize.js';
|
|
7
7
|
// Normalization utilities (server-only, requires Node.js path module)
|
|
8
8
|
export { validateAndNormalizePath } from './normalize-server.js';
|
|
9
|
-
export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseSlug, } from './validation.js';
|
|
9
|
+
export { validateContentPath, isValidCollectionPath, sanitizeForPath, hasEmbeddedContentId, looksLikePhysicalPath, looksLikeLogicalPath, parseLogicalPath, parsePhysicalPath, isValidContentId, parseContentId, parseBranchName, parseBranchRef, parseCreatedBranchName, parseSlug, } from './validation.js';
|
|
10
10
|
export { resolveLogicalPath } from './resolve.js';
|
|
11
11
|
export { resolveBranchPath, getDefaultBranchBase, resolveBranchPaths, BranchPathError, namesNoWorkspace, } from './branch.js';
|
|
12
|
-
export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX,
|
|
12
|
+
export { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, branchShortName, isSettingsBranchName, } from './branch-name.js';
|
package/dist/paths/resolve.d.ts
CHANGED
|
@@ -11,6 +11,5 @@ import type { LogicalPath } from './types.js';
|
|
|
11
11
|
* @example
|
|
12
12
|
* resolveLogicalPath("content/docs.ABC/api.DEF", schemaItems)
|
|
13
13
|
* // Returns: "content/docs/api"
|
|
14
|
-
* @internal Exported for tests.
|
|
15
14
|
*/
|
|
16
15
|
export declare function resolveLogicalPath(physicalPath: string, schemaItems: Iterable<FlatSchemaItem>): LogicalPath | string;
|
package/dist/paths/resolve.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* @example
|
|
10
10
|
* resolveLogicalPath("content/docs.ABC/api.DEF", schemaItems)
|
|
11
11
|
* // Returns: "content/docs/api"
|
|
12
|
-
* @internal Exported for tests.
|
|
13
12
|
*/
|
|
14
13
|
export function resolveLogicalPath(physicalPath, schemaItems) {
|
|
15
14
|
const pathSegments = physicalPath.split('/');
|
|
@@ -64,7 +64,20 @@ export declare function parseContentId(id: string): {
|
|
|
64
64
|
ok: false;
|
|
65
65
|
error: string;
|
|
66
66
|
};
|
|
67
|
-
/**
|
|
67
|
+
/** Whether `param` is in the exact form, well formed or not: no branch name starts with `~`. */
|
|
68
|
+
export declare function isExactBranchForm(param: string): boolean;
|
|
69
|
+
/** The exact form of the branch whose full name is `fullName`. */
|
|
70
|
+
export declare function exactBranchSegment(fullName: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The full name the exact form `param` names, or null when `param` is no well-formed exact form:
|
|
73
|
+
* it starts with `~`, holds no `/`, and with each later `~` read as `/` names a branch
|
|
74
|
+
* `parseBranchRef` takes, which refuses an empty name or part (`~`, `~~a`, `~a~`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function exactBranchName(param: string): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Validate a branch named in a request, by a `/:branch` segment or `?branch=`, and cast it to
|
|
79
|
+
* BranchName: a name `parseBranchRef` takes, or a well-formed exact form of one.
|
|
80
|
+
*/
|
|
68
81
|
export declare function parseBranchName(name: string): {
|
|
69
82
|
ok: true;
|
|
70
83
|
name: BranchName;
|
|
@@ -72,6 +85,32 @@ export declare function parseBranchName(name: string): {
|
|
|
72
85
|
ok: false;
|
|
73
86
|
error: string;
|
|
74
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Validate a branch's own name, the ref git and branch.json hold, against git's ref rules, except
|
|
90
|
+
* that an inner part may start with `.` or end `.lock` (`a/.b`, `a.lock/b`), and cast it to
|
|
91
|
+
* BranchName. It refuses every `~`, so the exact form is never one.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseBranchRef(name: string): {
|
|
94
|
+
ok: true;
|
|
95
|
+
name: BranchName;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
error: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The branch an editor's typed name creates in `namespace`, or why it cannot: the typed name
|
|
102
|
+
* sanitized, so it holds no `/`, is the short name, and the ref is the namespace followed by it.
|
|
103
|
+
* `createBranchHandler` passes its resolved namespace; the editor, which is never told one, passes
|
|
104
|
+
* none and checks the typed name alone, so its length limit is the looser one.
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseCreatedBranchName(typed: string, namespace?: string): {
|
|
107
|
+
ok: true;
|
|
108
|
+
name: BranchName;
|
|
109
|
+
shortName: string;
|
|
110
|
+
} | {
|
|
111
|
+
ok: false;
|
|
112
|
+
error: string;
|
|
113
|
+
};
|
|
75
114
|
/** Validate a collection or entry slug and cast it to Slug. */
|
|
76
115
|
export declare function parseSlug(slug: string): {
|
|
77
116
|
ok: true;
|
package/dist/paths/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Security-focused validation for content paths, IDs, branch names and slugs. */
|
|
2
|
+
import { sanitizeBranchName } from './branch-name.js';
|
|
2
3
|
import { normalizeFilesystemPath, hasTraversalSequence } from './normalize.js';
|
|
3
4
|
/** Base58 alphabet for content IDs: excludes the ambiguous 0, O, I and l. */
|
|
4
5
|
const BASE58_PATTERN = '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]';
|
|
@@ -133,14 +134,60 @@ export function parseContentId(id) {
|
|
|
133
134
|
}
|
|
134
135
|
return { ok: true, id: id };
|
|
135
136
|
}
|
|
136
|
-
/**
|
|
137
|
+
/** The longest branch name taken, in characters: names become directory names. */
|
|
138
|
+
const MAX_BRANCH_NAME_LENGTH = 250;
|
|
139
|
+
/**
|
|
140
|
+
* What starts a branch's exact form: `~`, then its full name with each `/` written `~`, so
|
|
141
|
+
* `canopycms/prod/spring` is `~canopycms~prod~spring` and a branch outside the namespace named
|
|
142
|
+
* `spring` is `~spring`. That branch and the namespace branch `canopycms/prod/spring` both answer
|
|
143
|
+
* to `spring`, which opens the namespace one; `~spring` opens the other. Git refuses `~` in a
|
|
144
|
+
* branch name, so no other name starts with it and the mapping reverses exactly, and
|
|
145
|
+
* `encodeURIComponent` leaves `~` alone, so a request path carries the form without `%2F`.
|
|
146
|
+
*/
|
|
147
|
+
const EXACT_BRANCH_MARK = '~';
|
|
148
|
+
/** Whether `param` is in the exact form, well formed or not: no branch name starts with `~`. */
|
|
149
|
+
export function isExactBranchForm(param) {
|
|
150
|
+
return param.startsWith(EXACT_BRANCH_MARK);
|
|
151
|
+
}
|
|
152
|
+
/** The exact form of the branch whose full name is `fullName`. */
|
|
153
|
+
export function exactBranchSegment(fullName) {
|
|
154
|
+
return `${EXACT_BRANCH_MARK}${fullName.replaceAll('/', EXACT_BRANCH_MARK)}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The full name the exact form `param` names, or null when `param` is no well-formed exact form:
|
|
158
|
+
* it starts with `~`, holds no `/`, and with each later `~` read as `/` names a branch
|
|
159
|
+
* `parseBranchRef` takes, which refuses an empty name or part (`~`, `~~a`, `~a~`).
|
|
160
|
+
*/
|
|
161
|
+
export function exactBranchName(param) {
|
|
162
|
+
if (!isExactBranchForm(param) || param.includes('/'))
|
|
163
|
+
return null;
|
|
164
|
+
const named = parseBranchRef(param.slice(EXACT_BRANCH_MARK.length).replaceAll(EXACT_BRANCH_MARK, '/'));
|
|
165
|
+
return named.ok ? named.name : null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Validate a branch named in a request, by a `/:branch` segment or `?branch=`, and cast it to
|
|
169
|
+
* BranchName: a name `parseBranchRef` takes, or a well-formed exact form of one.
|
|
170
|
+
*/
|
|
137
171
|
export function parseBranchName(name) {
|
|
172
|
+
if (typeof name === 'string' && isExactBranchForm(name)) {
|
|
173
|
+
return exactBranchName(name) === null
|
|
174
|
+
? { ok: false, error: 'Branch name starts with "~" but names no branch' }
|
|
175
|
+
: { ok: true, name: name };
|
|
176
|
+
}
|
|
177
|
+
return parseBranchRef(name);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Validate a branch's own name, the ref git and branch.json hold, against git's ref rules, except
|
|
181
|
+
* that an inner part may start with `.` or end `.lock` (`a/.b`, `a.lock/b`), and cast it to
|
|
182
|
+
* BranchName. It refuses every `~`, so the exact form is never one.
|
|
183
|
+
*/
|
|
184
|
+
export function parseBranchRef(name) {
|
|
138
185
|
if (!name || typeof name !== 'string') {
|
|
139
186
|
return { ok: false, error: 'Branch name is required' };
|
|
140
187
|
}
|
|
141
|
-
// Length limit (branch names become directory names)
|
|
142
|
-
if (name.length >
|
|
143
|
-
return { ok: false, error:
|
|
188
|
+
// Length limit (branch names become directory names).
|
|
189
|
+
if (name.length > MAX_BRANCH_NAME_LENGTH) {
|
|
190
|
+
return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH} characters)` };
|
|
144
191
|
}
|
|
145
192
|
if (name.includes('..')) {
|
|
146
193
|
return { ok: false, error: 'Branch name cannot contain ".."' };
|
|
@@ -178,6 +225,38 @@ export function parseBranchName(name) {
|
|
|
178
225
|
}
|
|
179
226
|
return { ok: true, name: name };
|
|
180
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The branch an editor's typed name creates in `namespace`, or why it cannot: the typed name
|
|
230
|
+
* sanitized, so it holds no `/`, is the short name, and the ref is the namespace followed by it.
|
|
231
|
+
* `createBranchHandler` passes its resolved namespace; the editor, which is never told one, passes
|
|
232
|
+
* none and checks the typed name alone, so its length limit is the looser one.
|
|
233
|
+
*/
|
|
234
|
+
export function parseCreatedBranchName(typed, namespace = '') {
|
|
235
|
+
const valid = parseBranchRef(typed);
|
|
236
|
+
if (!valid.ok)
|
|
237
|
+
return valid;
|
|
238
|
+
if (namespace !== '' && typed.startsWith(namespace)) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: `Type only the branch's own name, without "${namespace}" in front: CanopyCMS adds it`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Without a leading '-' or '.', which a branch name may not start with (`parseBranchRef`).
|
|
245
|
+
const shortName = sanitizeBranchName(typed).replace(/^[-.]+/, '');
|
|
246
|
+
// "_" alone would name a branch whose short name is no word at all.
|
|
247
|
+
if (!/[a-zA-Z0-9]/.test(shortName)) {
|
|
248
|
+
return { ok: false, error: 'Branch name needs at least one letter (a–z) or digit' };
|
|
249
|
+
}
|
|
250
|
+
const ref = `${namespace}${shortName}`;
|
|
251
|
+
if (ref.length > MAX_BRANCH_NAME_LENGTH) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH - namespace.length} characters)`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const parsed = parseBranchRef(ref);
|
|
258
|
+
return parsed.ok ? { ok: true, name: parsed.name, shortName } : parsed;
|
|
259
|
+
}
|
|
181
260
|
/** Validate a collection or entry slug and cast it to Slug. */
|
|
182
261
|
export function parseSlug(slug) {
|
|
183
262
|
if (!slug) {
|
package/dist/pr-comments.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The worker action that re-renders a PR
|
|
3
|
-
* api/pr-comment-sync.ts, run by
|
|
2
|
+
* The worker action that re-renders both blocks of a PR body that change after a submit, the
|
|
3
|
+
* editor-comment summary and the review block: enqueued by api/pr-comment-sync.ts, run by
|
|
4
|
+
* worker/task-runner.ts. The action string is the queue's contract.
|
|
4
5
|
*/
|
|
5
6
|
export declare const SYNC_PR_COMMENTS_ACTION = "sync-pr-comments";
|
|
6
7
|
/**
|
|
7
|
-
* `body` with its comment summary re-rendered from `branchRoot`'s comments.json
|
|
8
|
-
*
|
|
8
|
+
* `body` with its comment summary re-rendered from `branchRoot`'s comments.json and its review
|
|
9
|
+
* block from the review in branch.json as stored, or undefined when there is nothing to write.
|
|
10
|
+
* The two blocks are replaced independently: a body without one still gets the other, and a
|
|
11
|
+
* file that is corrupt leaves its block as it is, with a warning, without stopping the other.
|
|
12
|
+
* A missing file is no failure. Any other read error, an IO fault that may pass, is thrown so
|
|
13
|
+
* that the caller retries or fails visibly.
|
|
9
14
|
*/
|
|
10
15
|
export declare function renderPrCommentsBody(options: {
|
|
11
16
|
branchRoot: string;
|
|
12
17
|
branch: string;
|
|
13
18
|
editorUrl?: string;
|
|
14
|
-
body: string
|
|
19
|
+
body: string;
|
|
20
|
+
warn: (message: string) => void;
|
|
15
21
|
}): Promise<string | undefined>;
|
|
16
22
|
/**
|
|
17
23
|
* The in-process lock every direct-path write of a branch's PR body holds (submit and comment
|
|
@@ -19,10 +25,12 @@ export declare function renderPrCommentsBody(options: {
|
|
|
19
25
|
*/
|
|
20
26
|
export declare function prBodyLockKey(branchRoot: string): string;
|
|
21
27
|
/**
|
|
22
|
-
* A submit's section with its comment
|
|
23
|
-
* whose
|
|
24
|
-
* ran while this one waited out a retry, would otherwise be
|
|
25
|
-
*
|
|
28
|
+
* A submit's section with its comment and review blocks re-rendered from disk, for a queued
|
|
29
|
+
* submit whose blocks were rendered when it was queued: a comment or review since
|
|
30
|
+
* then, or a sync task that ran while this one waited out a retry, would otherwise be
|
|
31
|
+
* overwritten. The review is the one stored, as in renderPrCommentsBody: a submit clears the
|
|
32
|
+
* last verdict before it writes the PR. A file that cannot be read leaves its block as it was
|
|
33
|
+
* queued.
|
|
26
34
|
*/
|
|
27
35
|
export declare function refreshCommentsBlock(options: {
|
|
28
36
|
branchRoot: string;
|
package/dist/pr-comments.js
CHANGED
|
@@ -1,19 +1,46 @@
|
|
|
1
|
-
import { CommentStore } from './comment-store.js';
|
|
1
|
+
import { CommentStore, CommentsFileMalformedError } from './comment-store.js';
|
|
2
2
|
import { getErrorMessage } from './utils/error.js';
|
|
3
|
-
import {
|
|
3
|
+
import { BranchMetadataCorruptError, readBranchMetadataFile } from './branch-metadata-file.js';
|
|
4
|
+
import { buildCommentsBlock, buildReviewBlock, mergeCommentsBlock, mergeReviewBlock, } from './submission-attribution.js';
|
|
4
5
|
/**
|
|
5
|
-
* The worker action that re-renders a PR
|
|
6
|
-
* api/pr-comment-sync.ts, run by
|
|
6
|
+
* The worker action that re-renders both blocks of a PR body that change after a submit, the
|
|
7
|
+
* editor-comment summary and the review block: enqueued by api/pr-comment-sync.ts, run by
|
|
8
|
+
* worker/task-runner.ts. The action string is the queue's contract.
|
|
7
9
|
*/
|
|
8
10
|
export const SYNC_PR_COMMENTS_ACTION = 'sync-pr-comments';
|
|
9
11
|
/**
|
|
10
|
-
* `body` with its comment summary re-rendered from `branchRoot`'s comments.json
|
|
11
|
-
*
|
|
12
|
+
* `body` with its comment summary re-rendered from `branchRoot`'s comments.json and its review
|
|
13
|
+
* block from the review in branch.json as stored, or undefined when there is nothing to write.
|
|
14
|
+
* The two blocks are replaced independently: a body without one still gets the other, and a
|
|
15
|
+
* file that is corrupt leaves its block as it is, with a warning, without stopping the other.
|
|
16
|
+
* A missing file is no failure. Any other read error, an IO fault that may pass, is thrown so
|
|
17
|
+
* that the caller retries or fails visibly.
|
|
12
18
|
*/
|
|
13
19
|
export async function renderPrCommentsBody(options) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const original = options.body;
|
|
21
|
+
let next = original;
|
|
22
|
+
try {
|
|
23
|
+
const threads = await new CommentStore(options.branchRoot).listThreadsStrict();
|
|
24
|
+
next =
|
|
25
|
+
mergeCommentsBlock(next, buildCommentsBlock(threads, options.branch, options.editorUrl)) ??
|
|
26
|
+
next;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (!(err instanceof SyntaxError || err instanceof CommentsFileMalformedError))
|
|
30
|
+
throw err;
|
|
31
|
+
options.warn(`Could not read the editor comments of ${options.branch}; the PR keeps its summary: ${getErrorMessage(err)}`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const file = await readBranchMetadataFile(options.branchRoot);
|
|
35
|
+
if (file)
|
|
36
|
+
next = mergeReviewBlock(next, buildReviewBlock(file.branch.review)) ?? next;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (!(err instanceof BranchMetadataCorruptError))
|
|
40
|
+
throw err;
|
|
41
|
+
options.warn(`Could not read the review state of ${options.branch}; the PR keeps its review block: ${getErrorMessage(err)}`);
|
|
42
|
+
}
|
|
43
|
+
return next === original ? undefined : next;
|
|
17
44
|
}
|
|
18
45
|
/**
|
|
19
46
|
* The in-process lock every direct-path write of a branch's PR body holds (submit and comment
|
|
@@ -23,19 +50,30 @@ export function prBodyLockKey(branchRoot) {
|
|
|
23
50
|
return `pr-body:${branchRoot}`;
|
|
24
51
|
}
|
|
25
52
|
/**
|
|
26
|
-
* A submit's section with its comment
|
|
27
|
-
* whose
|
|
28
|
-
* ran while this one waited out a retry, would otherwise be
|
|
29
|
-
*
|
|
53
|
+
* A submit's section with its comment and review blocks re-rendered from disk, for a queued
|
|
54
|
+
* submit whose blocks were rendered when it was queued: a comment or review since
|
|
55
|
+
* then, or a sync task that ran while this one waited out a retry, would otherwise be
|
|
56
|
+
* overwritten. The review is the one stored, as in renderPrCommentsBody: a submit clears the
|
|
57
|
+
* last verdict before it writes the PR. A file that cannot be read leaves its block as it was
|
|
58
|
+
* queued.
|
|
30
59
|
*/
|
|
31
60
|
export async function refreshCommentsBlock(options) {
|
|
61
|
+
let section = options.section;
|
|
32
62
|
try {
|
|
33
63
|
const threads = await new CommentStore(options.branchRoot).listThreadsStrict();
|
|
34
64
|
const block = buildCommentsBlock(threads, options.branch, options.editorUrl);
|
|
35
|
-
|
|
65
|
+
section = mergeCommentsBlock(section, block) ?? section;
|
|
36
66
|
}
|
|
37
67
|
catch (err) {
|
|
38
68
|
options.warn(`Could not re-read the editor comments of ${options.branch}; the PR gets the summary from submit time: ${getErrorMessage(err)}`);
|
|
39
|
-
return options.section;
|
|
40
69
|
}
|
|
70
|
+
try {
|
|
71
|
+
const file = await readBranchMetadataFile(options.branchRoot);
|
|
72
|
+
if (file)
|
|
73
|
+
section = mergeReviewBlock(section, buildReviewBlock(file.branch.review)) ?? section;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
options.warn(`Could not re-read the review state of ${options.branch}; the PR gets it from submit time: ${getErrorMessage(err)}`);
|
|
77
|
+
}
|
|
78
|
+
return section;
|
|
41
79
|
}
|
package/dist/services.js
CHANGED
|
@@ -29,6 +29,7 @@ import { BranchSchemaCache } from './branch-schema-cache.js';
|
|
|
29
29
|
import { enqueueTask } from './task-queue/cms-task-queue.js';
|
|
30
30
|
import { getTaskQueueDir } from './task-queue/task-queue-config.js';
|
|
31
31
|
import { baseBranchOf } from './utils/base-branch.js';
|
|
32
|
+
import { shortNameOf } from './branch-param.js';
|
|
32
33
|
import { detectHeadBranch, isCanopyInternalPath } from './utils/git.js';
|
|
33
34
|
import { readsFromCheckout } from './build-mode.js';
|
|
34
35
|
import { timeRequestPhase } from './utils/request-timing.js';
|
|
@@ -330,7 +331,7 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
330
331
|
if (changedPaths?.length === 0) {
|
|
331
332
|
if (committed)
|
|
332
333
|
await git.resetKeepingChanges(preCommitSha);
|
|
333
|
-
throw new NothingToSubmitError(options.context.branch.name, effectiveBase);
|
|
334
|
+
throw new NothingToSubmitError(shortNameOf(config, options.context.branch.name), effectiveBase);
|
|
334
335
|
}
|
|
335
336
|
// A failure here leaves them uncommitted, so the next commit names them again.
|
|
336
337
|
if (committed && uncommitted.length > 0) {
|