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
package/dist/api/branch.js
CHANGED
|
@@ -12,11 +12,15 @@ import { clientOperatingStrategy } from '../operating-mode/index.js';
|
|
|
12
12
|
import { isNotFoundError, getErrorMessage, sanitizeErrorMessage } from '../utils/error.js';
|
|
13
13
|
import { filePathExists } from '../utils/fs.js';
|
|
14
14
|
import { isNetworkRemoteUrl } from '../utils/git.js';
|
|
15
|
-
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX,
|
|
15
|
+
import { parseCreatedBranchName, sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, isSettingsBranchName, } from '../paths/index.js';
|
|
16
16
|
import { GitManager } from '../git-manager.js';
|
|
17
17
|
import { syncDeleteRemoteBranch } from './github-sync.js';
|
|
18
18
|
import { branchNameSchema, branchParamSchema } from './validators.js';
|
|
19
19
|
import { baseBranchOf } from '../utils/base-branch.js';
|
|
20
|
+
import { shortNameOf } from '../branch-param.js';
|
|
21
|
+
import { namespaceAncestors } from '../operating-mode/branch-namespace.js';
|
|
22
|
+
import { getTaskQueueDir } from '../task-queue/task-queue-config.js';
|
|
23
|
+
import { readWorkerStatus } from '../task-queue/worker-status.js';
|
|
20
24
|
import { lookupUserProfiles } from '../auth/user-metadata-lookup.js';
|
|
21
25
|
const log = createDebugLogger({ prefix: 'BranchAPI' });
|
|
22
26
|
const createBranchBodySchema = z.object({
|
|
@@ -122,8 +126,11 @@ const accessForViewer = (access, viewer) => {
|
|
|
122
126
|
*/
|
|
123
127
|
export const toBranchListItem = (config, branch, viewer) => {
|
|
124
128
|
const protection = getBranchWriteProtection(config, branch.name, branch.baseBranch, branch.status);
|
|
129
|
+
// `review` names the reviewer, whom the editor has no use for.
|
|
130
|
+
const { review: _review, ...listed } = branch;
|
|
125
131
|
return {
|
|
126
|
-
...
|
|
132
|
+
...listed,
|
|
133
|
+
shortName: shortNameOf(config, branch.name),
|
|
127
134
|
...accessForViewer(branch.access ?? {}, viewer),
|
|
128
135
|
isProtected: protection.isProtected,
|
|
129
136
|
readOnly: protection.readOnly,
|
|
@@ -131,85 +138,116 @@ export const toBranchListItem = (config, branch, viewer) => {
|
|
|
131
138
|
submitBlocked: protection.submitBlockedIncludingStatus,
|
|
132
139
|
};
|
|
133
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* The API's static top-level route segments, which the router ranks above `/:branch`, so a branch
|
|
143
|
+
* whose short name is one would be shadowed. `http/router.test.ts` holds this list to the router.
|
|
144
|
+
* @internal Exported for tests.
|
|
145
|
+
*/
|
|
146
|
+
export const TOP_LEVEL_ROUTE_SEGMENTS = [
|
|
147
|
+
'admin',
|
|
148
|
+
'assets',
|
|
149
|
+
'branches',
|
|
150
|
+
'groups',
|
|
151
|
+
'permissions',
|
|
152
|
+
'users',
|
|
153
|
+
'whoami',
|
|
154
|
+
];
|
|
155
|
+
/**
|
|
156
|
+
* The names of the branches outside `namespace` a URL can reach, which neither a new branch's
|
|
157
|
+
* short name nor its ref may take, since the base and active branches resolve first and a short
|
|
158
|
+
* name would shadow the rest (branch-param.ts): the base, the configured active branch, every
|
|
159
|
+
* workspace outside the namespace, and the exact names the GitHub gateway protects as the last git
|
|
160
|
+
* sync recorded them in worker-status.json. Namespace
|
|
161
|
+
* workspaces are left out, so a creator's retry of a create that already landed reaches the key
|
|
162
|
+
* check's idempotent answer instead of a refusal.
|
|
163
|
+
*/
|
|
164
|
+
const outsideBranchNames = (config, namespace, listed, gatewayProtected) => {
|
|
165
|
+
const base = baseBranchOf(config);
|
|
166
|
+
return new Set([
|
|
167
|
+
base,
|
|
168
|
+
config.defaultActiveBranch ?? base,
|
|
169
|
+
...listed.map((context) => context.branch.name).filter((name) => !name.startsWith(namespace)),
|
|
170
|
+
...gatewayProtected,
|
|
171
|
+
]);
|
|
172
|
+
};
|
|
134
173
|
/** @internal Exported for tests. */
|
|
135
174
|
export const createBranchHandler = async (ctx, req, body) => {
|
|
136
175
|
return log.timed('api', 'createBranch', async () => {
|
|
137
|
-
const
|
|
176
|
+
const { config } = ctx.services;
|
|
177
|
+
const strategy = operatingStrategy(config.mode);
|
|
178
|
+
// Every branch an editor creates is in the namespace the GitHub gateway writes
|
|
179
|
+
// (worker/branch-policy.ts): its ref is the namespace followed by the typed name, sanitized.
|
|
180
|
+
let namespace;
|
|
181
|
+
try {
|
|
182
|
+
namespace = strategy.getBranchNamespace(config);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
status: 500,
|
|
188
|
+
error: `Branches cannot be created until an administrator fixes the branch namespace: ${getErrorMessage(err)}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const { workerStatus } = await readWorkerStatus(getTaskQueueDir(config));
|
|
192
|
+
const gatewayNamespace = workerStatus?.gatewayBranchNamespace;
|
|
193
|
+
// The worker reports the namespace once a git sync reads one from the gateway's health
|
|
194
|
+
// (worker/git-sync.ts). A worker that predates the namespace never reports one, and its rebase
|
|
195
|
+
// loop renames a namespace branch to its directory, which no request then resolves. Where
|
|
196
|
+
// branches publish through the worker, creation waits for the report.
|
|
197
|
+
if (gatewayNamespace === undefined && strategy.supportsPullRequests()) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
status: 503,
|
|
201
|
+
error: "Branches cannot be created yet: the CMS worker has not reported the GitHub gateway's " +
|
|
202
|
+
'branch namespace. Ask an administrator to check that the worker (canopycms-cdk) runs ' +
|
|
203
|
+
'this release and that its git sync reaches the gateway, then try again.',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (gatewayNamespace !== undefined && gatewayNamespace !== namespace) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
status: 409,
|
|
210
|
+
error: `Branches cannot be created: this CMS creates them in "${namespace}", but the GitHub ` +
|
|
211
|
+
`gateway publishes only branches in "${gatewayNamespace}", so none could be published. ` +
|
|
212
|
+
'Ask an administrator to make the two agree.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const named = parseCreatedBranchName(body.branch, namespace);
|
|
216
|
+
if (!named.ok)
|
|
217
|
+
return nameRefused(400, named.error);
|
|
218
|
+
const { name: branchName, shortName } = named;
|
|
219
|
+
const key = sanitizeBranchName(branchName);
|
|
138
220
|
log.debug('api', 'Create branch request', {
|
|
139
221
|
branchName,
|
|
140
222
|
userId: req.user.userId,
|
|
141
223
|
});
|
|
142
224
|
// Scope note: the remote-mirror check further down applies ONLY to this
|
|
143
225
|
// user-facing creation path; http/handler.ts's auto-create (base/active
|
|
144
|
-
// branches) and loadOrCreateBranchContext provision known names.
|
|
145
|
-
// settings-branch checks below give this path a specific 400; every
|
|
226
|
+
// branches) and loadOrCreateBranchContext provision known names. Every
|
|
146
227
|
// provisioning path, this one included, also refuses a settings branch in
|
|
147
228
|
// BranchWorkspaceManager.openOrCreateBranch.
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// openOrCreateBranch (branch-workspace.ts) uses the SANITIZED name as the
|
|
151
|
-
// actual git branch name. parseBranchName (via branchNameSchema) permits
|
|
152
|
-
// '/', so a raw-string comparison here let a request for
|
|
153
|
-
// "canopycms/settings-prod" sail past this check while
|
|
154
|
-
// sanitizeBranchName() collapsed it to "canopycms-settings-prod" --
|
|
155
|
-
// creating a content branch whose real git ref WAS the settings branch.
|
|
156
|
-
// Comparing sanitized forms on both sides closes that bypass.
|
|
157
|
-
const strategy = operatingStrategy(ctx.services.config.mode);
|
|
158
|
-
const sanitizedRequested = sanitizeBranchName(branchName);
|
|
229
|
+
// Settings live in a separate directory but share the git remote, so a content branch may not
|
|
230
|
+
// take the settings branch's ref. Compared sanitized, the form git and disk use.
|
|
159
231
|
if (strategy.usesSeparateSettingsBranch()) {
|
|
160
|
-
const settingsBranchName = strategy.getSettingsBranchName(
|
|
161
|
-
if (
|
|
162
|
-
return
|
|
163
|
-
ok: false,
|
|
164
|
-
status: 400,
|
|
165
|
-
error: 'Cannot create content branch with settings branch name (git branch name collision)',
|
|
166
|
-
};
|
|
232
|
+
const settingsBranchName = strategy.getSettingsBranchName(config);
|
|
233
|
+
if (key === sanitizeBranchName(settingsBranchName)) {
|
|
234
|
+
return nameRefused(400, 'Cannot create content branch with settings branch name (git branch name collision)');
|
|
167
235
|
}
|
|
168
236
|
}
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// `canopycms-settings-*` ref specially (orphan-branch reconcile/push
|
|
174
|
-
// logic), so another deployment's settings branch is a real name that
|
|
175
|
-
// must not be claimable as a content branch here either.
|
|
176
|
-
if (sanitizedRequested.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
|
|
177
|
-
return {
|
|
178
|
-
ok: false,
|
|
179
|
-
status: 400,
|
|
180
|
-
error: `Branch names starting with "${RESERVED_SETTINGS_BRANCH_PREFIX}" are reserved for CanopyCMS settings branches`,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// Reject names that collide with a static top-level API route namespace --
|
|
184
|
-
// the router prefers a literal segment over `:branch`, so such a branch
|
|
185
|
-
// would be unreachable through its own routes (see
|
|
186
|
-
// RESERVED_ROUTE_BRANCH_NAMES). Checked on both the raw and sanitized form
|
|
187
|
-
// for the same reason as the settings-branch guard above: the raw name is
|
|
188
|
-
// what lands in the `/:branch` URL segment, the sanitized name is what
|
|
189
|
-
// becomes the actual git ref.
|
|
190
|
-
if (RESERVED_ROUTE_BRANCH_NAMES.includes(branchName) ||
|
|
191
|
-
RESERVED_ROUTE_BRANCH_NAMES.includes(sanitizedRequested)) {
|
|
192
|
-
return {
|
|
193
|
-
ok: false,
|
|
194
|
-
status: 400,
|
|
195
|
-
error: `Branch name "${branchName}" is reserved: it collides with the /${branchName} API route namespace`,
|
|
196
|
-
};
|
|
237
|
+
// The handler answers any `/:branch` segment in the settings range as not found
|
|
238
|
+
// (http/handler.ts), so a branch with such a short name could never be opened.
|
|
239
|
+
if (isSettingsBranchName(shortName, strategy.getSettingsBranchName(config))) {
|
|
240
|
+
return nameRefused(400, `"${shortName}" is a name CanopyCMS keeps for its settings. Choose a different name.`);
|
|
197
241
|
}
|
|
198
242
|
// Reject the base branch name outright, even before the base branch is
|
|
199
243
|
// provisioned. No recorded fork point exists yet for a not-yet-created
|
|
200
244
|
// branch, so this checks config protection only.
|
|
201
|
-
const { isProtected } = getBranchProtection(
|
|
202
|
-
if (isProtected)
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
error: 'Cannot create a branch with the base branch name',
|
|
207
|
-
};
|
|
245
|
+
const { isProtected } = getBranchProtection(config, branchName);
|
|
246
|
+
if (isProtected)
|
|
247
|
+
return nameRefused(400, 'Cannot create a branch with the base branch name');
|
|
248
|
+
if (TOP_LEVEL_ROUTE_SEGMENTS.includes(shortName)) {
|
|
249
|
+
return nameRefused(400, `"${shortName}" is a name CanopyCMS uses for itself. Choose a different name.`);
|
|
208
250
|
}
|
|
209
|
-
// A name collision with ANY existing branch answers before a clone is paid
|
|
210
|
-
// for. Provisioning never merges into an existing branch either: it reports
|
|
211
|
-
// `exists`, mapped the same way below. Compared sanitized, the form
|
|
212
|
-
// branch.json records.
|
|
213
251
|
if (!ctx.services.registry) {
|
|
214
252
|
return {
|
|
215
253
|
ok: false,
|
|
@@ -217,9 +255,17 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
217
255
|
error: 'Branch registry not initialized — ensure the workspace has been initialized',
|
|
218
256
|
};
|
|
219
257
|
}
|
|
220
|
-
const
|
|
258
|
+
const listed = await ctx.services.registry.list();
|
|
259
|
+
const outside = outsideBranchNames(config, namespace, listed, workerStatus?.gatewayProtectedBranches ?? []);
|
|
260
|
+
if (outside.has(shortName) || outside.has(branchName)) {
|
|
261
|
+
return nameRefused(409, `"${shortName}" is already the name of another branch. Choose a different name.`);
|
|
262
|
+
}
|
|
263
|
+
// A name collision with ANY existing branch answers before a clone is paid
|
|
264
|
+
// for. Provisioning never merges into an existing branch either: it reports
|
|
265
|
+
// `exists`, mapped the same way below. Compared by key, the workspace directory.
|
|
266
|
+
const existingBranch = await ctx.services.registry.getByKey(key);
|
|
221
267
|
if (existingBranch) {
|
|
222
|
-
return existingBranchResponse(ctx, req, existingBranch);
|
|
268
|
+
return existingBranchResponse(ctx, req, existingBranch, named);
|
|
223
269
|
}
|
|
224
270
|
// L2: create-time collision check against this deployment's local
|
|
225
271
|
// GitHub mirror (`remote.git`). The CMS Lambda has no internet access
|
|
@@ -231,10 +277,11 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
231
277
|
// then reconciles refs/heads/* without touching editor work, so it carries both
|
|
232
278
|
// this deployment's local heads and GitHub's view -- readable offline by
|
|
233
279
|
// this same Lambda, since it resolves to the same EFS inode the worker
|
|
234
|
-
// uses. Reading it here catches a
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
280
|
+
// uses. Reading it here catches a collision with a branch another
|
|
281
|
+
// CanopyCMS deployment sharing this repo (or a direct push to GitHub)
|
|
282
|
+
// already created -- something the local registry check above cannot
|
|
283
|
+
// see -- and a branch named after part of the namespace, which git
|
|
284
|
+
// cannot hold beside any branch in it.
|
|
238
285
|
//
|
|
239
286
|
const resolvedMirrorPath = resolveReadOnlyMirrorPath(ctx, strategy);
|
|
240
287
|
if (!resolvedMirrorPath) {
|
|
@@ -266,7 +313,24 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
266
313
|
}
|
|
267
314
|
else {
|
|
268
315
|
try {
|
|
269
|
-
const
|
|
316
|
+
for (const ancestor of namespaceAncestors(namespace)) {
|
|
317
|
+
// Local heads too: remote.git cannot take the new branch beside one either.
|
|
318
|
+
if (await GitManager.bareRemoteHasBranch(resolvedMirrorPath, ancestor)) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
status: 409,
|
|
322
|
+
error: `Branches cannot be created: a branch named "${ancestor}" exists on the remote, ` +
|
|
323
|
+
'and git cannot hold a branch inside another. Ask an administrator to rename or ' +
|
|
324
|
+
'delete it.',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const [inside] = await GitManager.bareRemoteBranchesInside(resolvedMirrorPath, branchName);
|
|
329
|
+
if (inside !== undefined) {
|
|
330
|
+
return nameRefused(409, `"${shortName}" cannot be created: the remote has a branch named "${inside}" inside ` +
|
|
331
|
+
'it, and git cannot hold both. Choose a different name.');
|
|
332
|
+
}
|
|
333
|
+
const collision = await GitManager.bareRemoteHasBranch(resolvedMirrorPath, branchName,
|
|
270
334
|
// GitHub's view only -- see bareRemoteHasBranch. A local head in
|
|
271
335
|
// remote.git survives an editor-side branch delete forever, so
|
|
272
336
|
// including refs/heads/* here would make the ordinary create ->
|
|
@@ -275,14 +339,10 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
275
339
|
// by the registry check above.
|
|
276
340
|
{ namespaces: 'tracking' });
|
|
277
341
|
if (collision) {
|
|
278
|
-
return {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`It may have been created by another CanopyCMS deployment sharing this ` +
|
|
283
|
-
`repository, pushed directly to GitHub, or left behind by an earlier branch ` +
|
|
284
|
-
`of the same name. Choose a different name.`,
|
|
285
|
-
};
|
|
342
|
+
return nameRefused(409, `A branch named "${shortName}" already exists on the remote. ` +
|
|
343
|
+
`It may have been created by another CanopyCMS deployment sharing this ` +
|
|
344
|
+
`repository, pushed directly to GitHub, or left behind by an earlier branch ` +
|
|
345
|
+
`of the same name. Choose a different name.`);
|
|
286
346
|
}
|
|
287
347
|
// No collision: GitHub does not have this name. Clear any STALE local
|
|
288
348
|
// head the mirror still carries for it, so this new branch's first
|
|
@@ -302,11 +362,11 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
302
362
|
// that is today's status quo, and the publish-time 409 message names
|
|
303
363
|
// this cause.
|
|
304
364
|
try {
|
|
305
|
-
await GitManager.deleteBareRemoteHead(resolvedMirrorPath,
|
|
365
|
+
await GitManager.deleteBareRemoteHead(resolvedMirrorPath, branchName);
|
|
306
366
|
}
|
|
307
367
|
catch (err) {
|
|
308
368
|
log.warn('api', 'Failed to clear stale mirror head at branch reuse', {
|
|
309
|
-
branch:
|
|
369
|
+
branch: branchName,
|
|
310
370
|
path: resolvedMirrorPath,
|
|
311
371
|
error: getErrorMessage(err),
|
|
312
372
|
});
|
|
@@ -327,11 +387,11 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
327
387
|
// Load path permissions from the base branch's JSON file (the resolved
|
|
328
388
|
// fork point — baked into config at service creation; dev-mode git HEAD
|
|
329
389
|
// when not explicitly configured)
|
|
330
|
-
const baseBranch = baseBranchOf(
|
|
390
|
+
const baseBranch = baseBranchOf(config);
|
|
331
391
|
const baseBranchContext = await ctx.getBranchContext(baseBranch);
|
|
332
392
|
let pathPermissions = [];
|
|
333
393
|
if (baseBranchContext) {
|
|
334
|
-
const operatingMode =
|
|
394
|
+
const operatingMode = config.mode;
|
|
335
395
|
pathPermissions = await loadPathPermissions(baseBranchContext.branchRoot, operatingMode);
|
|
336
396
|
}
|
|
337
397
|
const canCreate = canCreateBranch(req.user, pathPermissions);
|
|
@@ -359,13 +419,12 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
359
419
|
if (err instanceof BranchProvisioningBusyError) {
|
|
360
420
|
return { ok: false, status: 503, error: err.message };
|
|
361
421
|
}
|
|
362
|
-
if (err instanceof BranchDirOccupiedError)
|
|
363
|
-
return
|
|
364
|
-
}
|
|
422
|
+
if (err instanceof BranchDirOccupiedError)
|
|
423
|
+
return nameRefused(409, err.message);
|
|
365
424
|
throw err;
|
|
366
425
|
}
|
|
367
426
|
if (outcome.kind === 'exists')
|
|
368
|
-
return existingBranchResponse(ctx, req, outcome.context);
|
|
427
|
+
return existingBranchResponse(ctx, req, outcome.context, named);
|
|
369
428
|
log.debug('api', 'Branch created', { branchName: outcome.context.branch.name });
|
|
370
429
|
return {
|
|
371
430
|
ok: true,
|
|
@@ -374,8 +433,16 @@ export const createBranchHandler = async (ctx, req, body) => {
|
|
|
374
433
|
};
|
|
375
434
|
});
|
|
376
435
|
};
|
|
377
|
-
/**
|
|
378
|
-
|
|
436
|
+
/**
|
|
437
|
+
* A create whose key an existing workspace holds: 409, unless that workspace is the branch this
|
|
438
|
+
* create names (`named.name`) and the create is its creator's recent retry. A workspace with
|
|
439
|
+
* another stored name only shares the key, so answering with it would hand back another branch.
|
|
440
|
+
*/
|
|
441
|
+
const existingBranchResponse = (ctx, req, existing, named) => {
|
|
442
|
+
if (existing.branch.name !== named.name) {
|
|
443
|
+
return nameRefused(409, `"${named.shortName}" would clash with the existing branch ` +
|
|
444
|
+
`"${shortNameOf(ctx.services.config, existing.branch.name)}". Choose a different name.`);
|
|
445
|
+
}
|
|
379
446
|
if (isCreatorsRecentBranch(existing.branch, req.user.userId)) {
|
|
380
447
|
return {
|
|
381
448
|
ok: true,
|
|
@@ -383,8 +450,15 @@ const existingBranchResponse = (ctx, req, existing) => {
|
|
|
383
450
|
data: { branch: toBranchListItem(ctx.services.config, existing.branch, req.user) },
|
|
384
451
|
};
|
|
385
452
|
}
|
|
386
|
-
return
|
|
453
|
+
return nameRefused(409, `A branch named "${named.shortName}" already exists`);
|
|
387
454
|
};
|
|
455
|
+
/** A create refused for the name it was given, which the editor shows under its name field. */
|
|
456
|
+
const nameRefused = (status, error) => ({
|
|
457
|
+
ok: false,
|
|
458
|
+
status,
|
|
459
|
+
error,
|
|
460
|
+
code: 'BRANCH_NAME_REFUSED',
|
|
461
|
+
});
|
|
388
462
|
/** @internal Exported for tests. */
|
|
389
463
|
export const listBranchesHandler = async (ctx, req) => {
|
|
390
464
|
if (!ctx.services.registry) {
|
|
@@ -398,13 +472,13 @@ export const listBranchesHandler = async (ctx, req) => {
|
|
|
398
472
|
// http/handler.ts), so one left on disk is hidden rather than listed unopenable.
|
|
399
473
|
const settingsBranch = operatingStrategy(ctx.services.config.mode).getSettingsBranchName(ctx.services.config);
|
|
400
474
|
const allBranches = (await ctx.services.registry.list()).filter((context) => !isSettingsBranchName(context.branch.name, settingsBranch));
|
|
401
|
-
// The branch the editor should open when none is pinned via URL/config
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
475
|
+
// The branch the editor should open when none is pinned via URL/config: the name of the
|
|
476
|
+
// workspace the active branch resolves to, which the editor finds in the list. Read
|
|
477
|
+
// per-request so dev-mode refreshActiveBranch() updates are reflected.
|
|
478
|
+
const activeBranch = ctx.services.config.defaultActiveBranch ?? baseBranchOf(ctx.services.config);
|
|
479
|
+
const activeKey = sanitizeBranchName(activeBranch);
|
|
480
|
+
const defaultBranch = allBranches.find((context) => path.basename(context.branchRoot) === activeKey)?.branch.name ??
|
|
481
|
+
activeBranch;
|
|
408
482
|
// The access check is the rule: the list shows exactly the branches it grants, so the editor
|
|
409
483
|
// never lists a branch it cannot open, nor leaves out one it can.
|
|
410
484
|
const visibleBranches = allBranches.filter((context) => ctx.services.checkBranchAccess(context, req.user).allowed);
|
|
@@ -574,14 +648,14 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
574
648
|
!isNetworkRemoteUrl(mirrorPath) &&
|
|
575
649
|
(await filePathExists(mirrorPath))) {
|
|
576
650
|
try {
|
|
577
|
-
await GitManager.deleteBareRemoteHead(mirrorPath,
|
|
651
|
+
await GitManager.deleteBareRemoteHead(mirrorPath, branchContext.branch.name);
|
|
578
652
|
}
|
|
579
653
|
catch (err) {
|
|
580
654
|
// Client-facing copy is sanitized (API-H2 — no absolute EFS paths);
|
|
581
655
|
// the console line keeps the raw detail for server logs.
|
|
582
656
|
const warning = `Failed to remove the deleted branch's head from the local mirror: ${sanitizeErrorMessage(getErrorMessage(err))}`;
|
|
583
657
|
cleanupWarning = cleanupWarning ? `${cleanupWarning}; ${warning}` : warning;
|
|
584
|
-
console.warn(`CanopyCMS: failed to remove deleted branch's mirror head (branch ${
|
|
658
|
+
console.warn(`CanopyCMS: failed to remove deleted branch's mirror head (branch ${branchContext.branch.name}, mirror ${mirrorPath}): ${getErrorMessage(err)}`);
|
|
585
659
|
}
|
|
586
660
|
}
|
|
587
661
|
// `branchContext` was read before the delete, so it still carries the PR number this gates on.
|
package/dist/api/client.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { type RequestOptions } from './request-deadline.js';
|
|
|
9
9
|
export { isApiTimeoutError, WRITE_TIMED_OUT_MESSAGE, type RequestOptions } from './request-deadline.js';
|
|
10
10
|
import type { OperatingMode } from '../operating-mode/types.js';
|
|
11
11
|
import type { BranchDeleteResponse, BranchListItemResponse, BranchListResponse, CreateBranchBody, UpdateBranchAccessBody } from './branch.js';
|
|
12
|
+
import type { BranchChangesResponse } from './branch-changes.js';
|
|
13
|
+
import type { BranchEntryChangeResponse } from './branch-entry-changes.js';
|
|
12
14
|
import type { BranchMergeResponse, RequestChangesBody, SubmitBranchBody, SubmitBranchResponse } from './branch-status.js';
|
|
13
15
|
import type { AddCommentBody, AddCommentResponse, CommentsResponse, ResolveCommentResponse } from './comments.js';
|
|
14
16
|
import type { ContentReadResponse, ContentWriteResponse, ReferenceValidationResponse, RenameEntryBody, RenameEntryResponse, ValidateReferencesBody, WriteContentBody } from './content.js';
|
|
@@ -49,6 +51,12 @@ export interface ApiClientOptions {
|
|
|
49
51
|
editorMode?: OperatingMode;
|
|
50
52
|
/** Called whenever the server refuses `editorMode` (code `EDITOR_MODE_MISMATCH`). @internal */
|
|
51
53
|
onEditorModeMismatch?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Maps a branch name to the `/:branch` segment its requests carry. The editor maps each
|
|
56
|
+
* listed branch to its exact form (`~canopycms~prod~spring`, `exactBranchSegment`), which the
|
|
57
|
+
* server resolves to that branch alone, whatever the base or active branch is. @internal
|
|
58
|
+
*/
|
|
59
|
+
branchSegment?: (branch: string) => string;
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* Typed client for the CanopyCMS API: one method per endpoint, grouped by namespace, each
|
|
@@ -68,6 +76,7 @@ export declare class CanopyApiClient {
|
|
|
68
76
|
private onUnauthorized;
|
|
69
77
|
private editorMode;
|
|
70
78
|
private onEditorModeMismatch;
|
|
79
|
+
private branchSegment;
|
|
71
80
|
readonly branches: {
|
|
72
81
|
/** GET /branches */
|
|
73
82
|
list: (options?: RequestOptions) => Promise<BranchListResponse>;
|
|
@@ -78,6 +87,12 @@ export declare class CanopyApiClient {
|
|
|
78
87
|
/** PATCH /:branch/access */
|
|
79
88
|
updateAccess: (params: Record<string, string>, body: UpdateBranchAccessBody, options?: RequestOptions) => Promise<BranchListItemResponse>;
|
|
80
89
|
};
|
|
90
|
+
readonly changes: {
|
|
91
|
+
/** GET /:branch/changes */
|
|
92
|
+
list: (params: Record<string, string>, options?: RequestOptions) => Promise<BranchChangesResponse>;
|
|
93
|
+
/** GET /:branch/changes/...entryPath */
|
|
94
|
+
entry: (params: Record<string, string>, options?: RequestOptions) => Promise<BranchEntryChangeResponse>;
|
|
95
|
+
};
|
|
81
96
|
readonly workflow: {
|
|
82
97
|
/** POST /:branch/withdraw */
|
|
83
98
|
withdraw: (params: Record<string, string>, options?: RequestOptions) => Promise<BranchListItemResponse>;
|
package/dist/api/client.js
CHANGED
|
@@ -41,6 +41,16 @@ export class CanopyApiClient {
|
|
|
41
41
|
return this.request('PATCH', this.buildPath('/:branch/access', params), body, 'standard', options);
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
|
+
this.changes = {
|
|
45
|
+
/** GET /:branch/changes */
|
|
46
|
+
list: (params, options) => {
|
|
47
|
+
return this.request('GET', this.buildPath('/:branch/changes', params), undefined, 'standard', options);
|
|
48
|
+
},
|
|
49
|
+
/** GET /:branch/changes/...entryPath */
|
|
50
|
+
entry: (params, options) => {
|
|
51
|
+
return this.request('GET', this.buildPath('/:branch/changes/...entryPath', params), undefined, 'standard', options);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
44
54
|
this.workflow = {
|
|
45
55
|
/** POST /:branch/withdraw */
|
|
46
56
|
withdraw: (params, options) => {
|
|
@@ -277,6 +287,7 @@ export class CanopyApiClient {
|
|
|
277
287
|
this.onUnauthorized = options.onUnauthorized;
|
|
278
288
|
this.editorMode = options.editorMode;
|
|
279
289
|
this.onEditorModeMismatch = options.onEditorModeMismatch;
|
|
290
|
+
this.branchSegment = options.branchSegment;
|
|
280
291
|
}
|
|
281
292
|
buildPath(template, params) {
|
|
282
293
|
let result = template;
|
|
@@ -285,7 +296,8 @@ export class CanopyApiClient {
|
|
|
285
296
|
const pathParamPattern = `:${key}`;
|
|
286
297
|
const restParamPattern = `...${key}`;
|
|
287
298
|
if (result.includes(pathParamPattern)) {
|
|
288
|
-
|
|
299
|
+
const segment = key === 'branch' && this.branchSegment ? this.branchSegment(value) : value;
|
|
300
|
+
result = result.replace(pathParamPattern, encodeURIComponent(String(segment)));
|
|
289
301
|
}
|
|
290
302
|
else if (result.includes(restParamPattern)) {
|
|
291
303
|
// For rest parameters, encode each segment separately to preserve slashes
|
package/dist/api/content.d.ts
CHANGED
|
@@ -63,39 +63,39 @@ export declare const CONTENT_ROUTES: {
|
|
|
63
63
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
64
64
|
path: z.ZodType<LogicalPath, z.ZodTypeDef, LogicalPath>;
|
|
65
65
|
}, "strip", z.ZodTypeAny, {
|
|
66
|
-
path: string & {
|
|
67
|
-
readonly __brand: "LogicalPath";
|
|
68
|
-
};
|
|
69
66
|
branch: string & {
|
|
70
67
|
readonly __brand: "BranchName";
|
|
71
68
|
};
|
|
72
|
-
}, {
|
|
73
69
|
path: string & {
|
|
74
70
|
readonly __brand: "LogicalPath";
|
|
75
71
|
};
|
|
72
|
+
}, {
|
|
76
73
|
branch: string & {
|
|
77
74
|
readonly __brand: "BranchName";
|
|
78
75
|
};
|
|
76
|
+
path: string & {
|
|
77
|
+
readonly __brand: "LogicalPath";
|
|
78
|
+
};
|
|
79
79
|
}>, undefined, ContentReadResponse>;
|
|
80
80
|
readonly write: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
81
81
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
82
82
|
path: z.ZodType<LogicalPath, z.ZodTypeDef, LogicalPath>;
|
|
83
83
|
entryType: z.ZodOptional<z.ZodString>;
|
|
84
84
|
}, "strip", z.ZodTypeAny, {
|
|
85
|
-
path: string & {
|
|
86
|
-
readonly __brand: "LogicalPath";
|
|
87
|
-
};
|
|
88
85
|
branch: string & {
|
|
89
86
|
readonly __brand: "BranchName";
|
|
90
87
|
};
|
|
91
|
-
entryType?: string | undefined;
|
|
92
|
-
}, {
|
|
93
88
|
path: string & {
|
|
94
89
|
readonly __brand: "LogicalPath";
|
|
95
90
|
};
|
|
91
|
+
entryType?: string | undefined;
|
|
92
|
+
}, {
|
|
96
93
|
branch: string & {
|
|
97
94
|
readonly __brand: "BranchName";
|
|
98
95
|
};
|
|
96
|
+
path: string & {
|
|
97
|
+
readonly __brand: "LogicalPath";
|
|
98
|
+
};
|
|
99
99
|
entryType?: string | undefined;
|
|
100
100
|
}>, z.ZodObject<{
|
|
101
101
|
format: z.ZodEnum<["json", "md", "mdx", "yaml"]>;
|
|
@@ -118,20 +118,20 @@ export declare const CONTENT_ROUTES: {
|
|
|
118
118
|
path: z.ZodType<LogicalPath, z.ZodTypeDef, LogicalPath>;
|
|
119
119
|
entryType: z.ZodOptional<z.ZodString>;
|
|
120
120
|
}, "strip", z.ZodTypeAny, {
|
|
121
|
-
path: string & {
|
|
122
|
-
readonly __brand: "LogicalPath";
|
|
123
|
-
};
|
|
124
121
|
branch: string & {
|
|
125
122
|
readonly __brand: "BranchName";
|
|
126
123
|
};
|
|
127
|
-
entryType?: string | undefined;
|
|
128
|
-
}, {
|
|
129
124
|
path: string & {
|
|
130
125
|
readonly __brand: "LogicalPath";
|
|
131
126
|
};
|
|
127
|
+
entryType?: string | undefined;
|
|
128
|
+
}, {
|
|
132
129
|
branch: string & {
|
|
133
130
|
readonly __brand: "BranchName";
|
|
134
131
|
};
|
|
132
|
+
path: string & {
|
|
133
|
+
readonly __brand: "LogicalPath";
|
|
134
|
+
};
|
|
135
135
|
entryType?: string | undefined;
|
|
136
136
|
}>, z.ZodObject<{
|
|
137
137
|
data: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodUnknown>, Record<string, unknown>, Record<string, unknown>>;
|
|
@@ -144,19 +144,19 @@ export declare const CONTENT_ROUTES: {
|
|
|
144
144
|
branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
|
|
145
145
|
path: z.ZodType<LogicalPath, z.ZodTypeDef, LogicalPath>;
|
|
146
146
|
}, "strip", z.ZodTypeAny, {
|
|
147
|
-
path: string & {
|
|
148
|
-
readonly __brand: "LogicalPath";
|
|
149
|
-
};
|
|
150
147
|
branch: string & {
|
|
151
148
|
readonly __brand: "BranchName";
|
|
152
149
|
};
|
|
153
|
-
}, {
|
|
154
150
|
path: string & {
|
|
155
151
|
readonly __brand: "LogicalPath";
|
|
156
152
|
};
|
|
153
|
+
}, {
|
|
157
154
|
branch: string & {
|
|
158
155
|
readonly __brand: "BranchName";
|
|
159
156
|
};
|
|
157
|
+
path: string & {
|
|
158
|
+
readonly __brand: "LogicalPath";
|
|
159
|
+
};
|
|
160
160
|
}>, z.ZodObject<{
|
|
161
161
|
newSlug: z.ZodType<Slug, z.ZodTypeDef, Slug>;
|
|
162
162
|
}, "strip", z.ZodTypeAny, {
|
package/dist/api/content.js
CHANGED
|
@@ -429,7 +429,7 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
429
429
|
try {
|
|
430
430
|
issues = await validateEntry({
|
|
431
431
|
entryPath: logicalPathSegments.join('/'),
|
|
432
|
-
branch:
|
|
432
|
+
branch: branchContext.branch.name,
|
|
433
433
|
// The type the store writes with, so a request that omits `entryType` can't skip a
|
|
434
434
|
// type-gated rule.
|
|
435
435
|
...(entryTypeName ? { entryType: entryTypeName } : {}),
|
package/dist/api/github-sync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { sanitizeBranchName } from '../paths/branch-name.js';
|
|
|
6
6
|
import { buildPrSection, mergePrSection } from '../submission-attribution.js';
|
|
7
7
|
import { isNoCommitsBetweenError, isRefAlreadyGoneError } from '../github-service.js';
|
|
8
8
|
import { baseBranchOf } from '../utils/base-branch.js';
|
|
9
|
+
import { shortNameOf } from '../branch-param.js';
|
|
9
10
|
import { CommentStore } from '../comment-store.js';
|
|
10
11
|
import { prBodyLockKey, refreshCommentsBlock } from '../pr-comments.js';
|
|
11
12
|
import { withLock } from '../utils/async-mutex.js';
|
|
@@ -28,14 +29,16 @@ async function readThreadsOrWarn(context) {
|
|
|
28
29
|
export async function syncSubmitPr(ctx, context, submission) {
|
|
29
30
|
const { githubService } = ctx.services;
|
|
30
31
|
const mode = ctx.services.config.mode;
|
|
31
|
-
|
|
32
|
+
// Links and the title carry the short name, which `?branch=` resolves.
|
|
33
|
+
const shortName = shortNameOf(ctx.services.config, context.branch.name);
|
|
34
|
+
const prTitle = context.branch.title || `Submit ${shortName}`;
|
|
32
35
|
const threads = await readThreadsOrWarn(context);
|
|
33
36
|
const prSection = buildPrSection({
|
|
34
37
|
description: context.branch.description,
|
|
35
38
|
submitter: submission.submitter,
|
|
36
39
|
editors: submission.editors,
|
|
37
40
|
changedPaths: submission.changedPaths,
|
|
38
|
-
branch:
|
|
41
|
+
branch: shortName,
|
|
39
42
|
editorUrl: submission.editorUrl,
|
|
40
43
|
entryPaths: submission.entryPaths,
|
|
41
44
|
threads,
|
|
@@ -66,7 +69,7 @@ export async function syncSubmitPr(ctx, context, submission) {
|
|
|
66
69
|
// Re-read under the lock: a comment synced since the read above has the newer block.
|
|
67
70
|
const section = await refreshCommentsBlock({
|
|
68
71
|
branchRoot: context.branchRoot,
|
|
69
|
-
branch:
|
|
72
|
+
branch: shortName,
|
|
70
73
|
editorUrl: submission.editorUrl,
|
|
71
74
|
section: prSection,
|
|
72
75
|
warn: (message) => console.warn(`CanopyCMS: ${message}`),
|
|
@@ -159,6 +162,7 @@ export async function syncSubmitPr(ctx, context, submission) {
|
|
|
159
162
|
mergeSectionIntoBody: true,
|
|
160
163
|
// The worker re-renders the comment block from comments.json when the task runs.
|
|
161
164
|
...(submission.editorUrl ? { editorUrl: submission.editorUrl } : {}),
|
|
165
|
+
editorBranch: shortName,
|
|
162
166
|
baseBranch,
|
|
163
167
|
pullRequestNumber: context.branch.pullRequestNumber,
|
|
164
168
|
// A content submit is an explicit "ready for review" action: convert a
|