canopycms 0.0.67 → 0.0.68-int.94
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 +3 -2
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +21 -9
- package/dist/api/admin.js +18 -2
- package/dist/api/branch-status.js +19 -4
- package/dist/api/branch.js +11 -11
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +34 -11
- 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 +10 -1
- package/dist/api/github-sync.js +20 -10
- 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/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- 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.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +8 -3
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/cli.js +1070 -388
- package/dist/cli/generate-ai-content.js +537 -150
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +21 -11
- 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 +6 -0
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/schemas/config.d.ts +11 -3
- package/dist/config/schemas/config.js +5 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/url.d.ts +2 -0
- package/dist/config/schemas/url.js +7 -0
- package/dist/config/types.d.ts +28 -4
- 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/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.js +6 -5
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +45 -7
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/editor-utils.d.ts +17 -15
- package/dist/editor/editor-utils.js +43 -36
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useCommentSystem.js +5 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/editor/preview-bridge.js +4 -1
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- 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 +79 -5
- package/dist/git-manager.js +308 -18
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +35 -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/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -1
- package/dist/paths/branch.js +15 -2
- 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/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 +48 -22
- package/dist/schema/schema-store.js +66 -22
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.js +42 -4
- 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-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 +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- 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.js +23 -2
- package/dist/worker/git-sync.d.ts +8 -2
- package/dist/worker/git-sync.js +138 -24
- 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 +83 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
|
@@ -24,9 +24,10 @@ export type PermissionSet = Readonly<Record<string, PermissionLevel>>;
|
|
|
24
24
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
25
25
|
* this object states the whole surface, not just the non-automatic part.
|
|
26
26
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
27
|
-
* permissions. No `workflows` either
|
|
28
|
-
*
|
|
29
|
-
* .
|
|
27
|
+
* permissions. No `workflows` either: GitHub refuses only workflow content it
|
|
28
|
+
* does not already hold, so rebasing onto a base that changed a workflow is
|
|
29
|
+
* accepted. Content that is new to GitHub fails the push task with the file
|
|
30
|
+
* named (`throwIfWorkflowRefusal` in worker/task-runner.ts).
|
|
30
31
|
*/
|
|
31
32
|
export declare const CANOPY_APP_PERMISSIONS: PermissionSet;
|
|
32
33
|
/**
|
|
@@ -22,9 +22,10 @@ const PERMISSION_LEVELS = ['read', 'write', 'admin'];
|
|
|
22
22
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
23
23
|
* this object states the whole surface, not just the non-automatic part.
|
|
24
24
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
25
|
-
* permissions. No `workflows` either
|
|
26
|
-
*
|
|
27
|
-
* .
|
|
25
|
+
* permissions. No `workflows` either: GitHub refuses only workflow content it
|
|
26
|
+
* does not already hold, so rebasing onto a base that changed a workflow is
|
|
27
|
+
* accepted. Content that is new to GitHub fails the push task with the file
|
|
28
|
+
* named (`throwIfWorkflowRefusal` in worker/task-runner.ts).
|
|
28
29
|
*/
|
|
29
30
|
export const CANOPY_APP_PERMISSIONS = {
|
|
30
31
|
contents: 'write',
|
package/dist/cli/init.js
CHANGED
|
@@ -115,7 +115,8 @@ var init_field = __esm({
|
|
|
115
115
|
});
|
|
116
116
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
117
117
|
type: z.literal("object"),
|
|
118
|
-
fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
|
|
118
|
+
fields: z.array(z.lazy(() => fieldHolder[0])).min(1),
|
|
119
|
+
itemTitleField: z.string().min(1).optional()
|
|
119
120
|
});
|
|
120
121
|
inlineGroupFieldSchema = z.object({
|
|
121
122
|
type: z.literal("group"),
|
|
@@ -231,7 +232,7 @@ var init_sanitize_href = __esm({
|
|
|
231
232
|
|
|
232
233
|
// dist/config/schemas/url.js
|
|
233
234
|
import { z as z3 } from "zod";
|
|
234
|
-
var uploadTargetUrlSchema, assetMountUrlSchema;
|
|
235
|
+
var uploadTargetUrlSchema, assetMountUrlSchema, previewPrefixSchema;
|
|
235
236
|
var init_url = __esm({
|
|
236
237
|
"dist/config/schemas/url.js"() {
|
|
237
238
|
"use strict";
|
|
@@ -242,6 +243,9 @@ var init_url = __esm({
|
|
|
242
243
|
assetMountUrlSchema = z3.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value, { allowProtocolRelative: true }), {
|
|
243
244
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
244
245
|
});
|
|
246
|
+
previewPrefixSchema = z3.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
247
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
248
|
+
});
|
|
245
249
|
}
|
|
246
250
|
});
|
|
247
251
|
|
|
@@ -299,6 +303,7 @@ var init_config = __esm({
|
|
|
299
303
|
"use strict";
|
|
300
304
|
init_collection();
|
|
301
305
|
init_media();
|
|
306
|
+
init_url();
|
|
302
307
|
defaultBranchAccessSchema = z5.enum(["allow", "deny"]).default("deny");
|
|
303
308
|
defaultPathAccessLevelSchema = z5.enum(["allow", "deny"]);
|
|
304
309
|
defaultPathAccessLevelsSchema = z5.object({
|
|
@@ -330,6 +335,7 @@ var init_config = __esm({
|
|
|
330
335
|
subtitle: z5.string().optional(),
|
|
331
336
|
theme: z5.unknown().optional(),
|
|
332
337
|
previewBase: z5.record(z5.string()).optional(),
|
|
338
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
333
339
|
// UI handler functions (runtime only, don't serialize)
|
|
334
340
|
onAccountClick: z5.function().returns(z5.void()).optional(),
|
|
335
341
|
onLogoutClick: z5.function().returns(z5.void()).optional(),
|
|
@@ -357,6 +363,9 @@ var init_config = __esm({
|
|
|
357
363
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
358
364
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
359
365
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
366
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
367
|
+
gitEditedByTrailers: z5.boolean().optional(),
|
|
368
|
+
gitCoAuthoredByTrailers: z5.boolean().optional(),
|
|
360
369
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
361
370
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
362
371
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -368,7 +377,6 @@ var init_config = __esm({
|
|
|
368
377
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
369
378
|
allowNetworkRemoteInProd: z5.boolean().optional(),
|
|
370
379
|
settingsBranch: z5.string().optional(),
|
|
371
|
-
autoCreateSettingsPR: z5.boolean().optional(),
|
|
372
380
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
373
381
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
374
382
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -889,6 +897,10 @@ var init_debug = __esm({
|
|
|
889
897
|
throw new Error(errorMsg);
|
|
890
898
|
}
|
|
891
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
902
|
+
* anything that can run concurrently uses {@link timed}.
|
|
903
|
+
*/
|
|
892
904
|
time(label) {
|
|
893
905
|
this.timers.set(label, Date.now());
|
|
894
906
|
}
|
|
@@ -903,12 +915,16 @@ var init_debug = __esm({
|
|
|
903
915
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
904
916
|
return duration;
|
|
905
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
920
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
921
|
+
*/
|
|
906
922
|
async timed(category, label, fn) {
|
|
907
|
-
|
|
923
|
+
const start = Date.now();
|
|
908
924
|
try {
|
|
909
925
|
return await fn();
|
|
910
926
|
} finally {
|
|
911
|
-
this.
|
|
927
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
912
928
|
}
|
|
913
929
|
}
|
|
914
930
|
};
|
|
@@ -1525,9 +1541,6 @@ var ProdStrategy = class extends ProdClientSafeStrategy {
|
|
|
1525
1541
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
1526
1542
|
}
|
|
1527
1543
|
}
|
|
1528
|
-
shouldCreateSettingsPR(config) {
|
|
1529
|
-
return config.autoCreateSettingsPR ?? true;
|
|
1530
|
-
}
|
|
1531
1544
|
};
|
|
1532
1545
|
var DevStrategy = class extends DevClientSafeStrategy {
|
|
1533
1546
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -1574,9 +1587,6 @@ var DevStrategy = class extends DevClientSafeStrategy {
|
|
|
1574
1587
|
}
|
|
1575
1588
|
validateConfig(_config) {
|
|
1576
1589
|
}
|
|
1577
|
-
shouldCreateSettingsPR(_config) {
|
|
1578
|
-
return false;
|
|
1579
|
-
}
|
|
1580
1590
|
};
|
|
1581
1591
|
var strategyCache = /* @__PURE__ */ new Map();
|
|
1582
1592
|
function operatingStrategy(mode) {
|
package/dist/cli/migrate.js
CHANGED
|
@@ -21,8 +21,9 @@ import { parseSlug } from '../paths/index.js';
|
|
|
21
21
|
import { filePathExists } from '../utils/fs.js';
|
|
22
22
|
import { loadCollectionMetaFiles } from '../schema/index.js';
|
|
23
23
|
import { getErrorMessage } from '../utils/error.js';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { ContentWriteLockBusyError } from '../utils/content-write-lock.js';
|
|
25
|
+
import { OccWriteConflictError } from '../utils/occ-json-write.js';
|
|
26
|
+
import { withBranchSchemaLock } from '../schema/schema-store.js';
|
|
26
27
|
import { BranchMetadataFileManager } from '../branch-metadata.js';
|
|
27
28
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
28
29
|
const MIGRATE_FORMATS = ['md', 'mdx', 'json', 'yaml'];
|
|
@@ -245,18 +246,21 @@ export async function migrate(options) {
|
|
|
245
246
|
};
|
|
246
247
|
if (isBranchClone) {
|
|
247
248
|
// projectDir IS the branch root in this case (see the invariant note
|
|
248
|
-
// above)
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// serializes against it instead of silently interleaving writes.
|
|
252
|
-
const lockPath = path.join(projectDir, '.canopy-meta', 'schema');
|
|
249
|
+
// above). The renames rewrite the working tree the worker rebases, so this
|
|
250
|
+
// takes the same locks SchemaOps does for its own schema mutations: the
|
|
251
|
+
// content-write lock, then the schema surrogate.
|
|
253
252
|
try {
|
|
254
|
-
await
|
|
253
|
+
await withBranchSchemaLock(projectDir, applyOps);
|
|
255
254
|
}
|
|
256
255
|
catch (err) {
|
|
257
256
|
if (err instanceof OccWriteConflictError) {
|
|
258
257
|
throw new MigrateError('Another process is modifying the schema, try again.');
|
|
259
258
|
}
|
|
259
|
+
if (err instanceof ContentWriteLockBusyError) {
|
|
260
|
+
throw new MigrateError(err.outcome === 'unknown'
|
|
261
|
+
? 'The branch was being synced while migrate ran, so it may be partly applied. Check its state before running migrate again.'
|
|
262
|
+
: err.message);
|
|
263
|
+
}
|
|
260
264
|
throw err;
|
|
261
265
|
}
|
|
262
266
|
}
|
package/dist/cli/sync.js
CHANGED
|
@@ -17,8 +17,10 @@ import path from 'node:path';
|
|
|
17
17
|
import { simpleGit } from 'simple-git';
|
|
18
18
|
import * as p from '@clack/prompts';
|
|
19
19
|
import { filePathExists } from '../utils/fs.js';
|
|
20
|
+
import { isCanopyInternalPath, stageAllExceptCanopyState } from '../utils/git.js';
|
|
20
21
|
import { assertWithinDir, copyDir, listFilesRecursive, pushContentToWorkspace, safeReplaceDir, SYNC_BASE_TAG, } from '../sync-core.js';
|
|
21
22
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
23
|
+
import { ContentWriteLockBusyError, withContentWriteLock } from '../utils/content-write-lock.js';
|
|
22
24
|
import { ensureGitExcludePattern } from '../git-manager.js';
|
|
23
25
|
import { operatingStrategy } from '../operating-mode/index.js';
|
|
24
26
|
/**
|
|
@@ -32,6 +34,42 @@ class SyncError extends Error {
|
|
|
32
34
|
this.name = 'SyncError';
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* [SYNC-C1] Every sync step that rewrites a branch workspace (content replace, checkout, merge,
|
|
39
|
+
* merge abort) runs under the workspace's content-write lock, which the dev server's saves and
|
|
40
|
+
* the worker's rebase also take. Contention becomes a `SyncError`, so the CLI exits non-zero.
|
|
41
|
+
*/
|
|
42
|
+
function lockBusyAsSyncError(err, branchName) {
|
|
43
|
+
if (!(err instanceof ContentWriteLockBusyError))
|
|
44
|
+
return err;
|
|
45
|
+
return new SyncError(err.outcome === 'not-run'
|
|
46
|
+
? `Branch workspace "${branchName}" is busy (the worker is syncing it, or an editor save is in flight); nothing was changed. Try again in a moment.`
|
|
47
|
+
: `Branch workspace "${branchName}" was being synced during this run, so it may be partly updated. Check its state before running sync again.`);
|
|
48
|
+
}
|
|
49
|
+
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
50
|
+
try {
|
|
51
|
+
return await withContentWriteLock(branchPath, fn);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw lockBusyAsSyncError(err, branchName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `git merge --abort` in a workspace left mid-merge, re-checking under the lock that it still is.
|
|
59
|
+
* Returns whether a merge was aborted.
|
|
60
|
+
*/
|
|
61
|
+
async function abortWorkspaceMerge(branchPath, branchName) {
|
|
62
|
+
return withWorkspaceLock(branchPath, branchName, async () => {
|
|
63
|
+
const wsGit = simpleGit({ baseDir: branchPath });
|
|
64
|
+
if ((await wsGit.status()).conflicted.length === 0)
|
|
65
|
+
return false;
|
|
66
|
+
await wsGit.merge(['--abort']);
|
|
67
|
+
// The abort rewrote the clone's working tree — tell ContentStore ID
|
|
68
|
+
// indexes (the dev server is a separate process; on-disk marker).
|
|
69
|
+
await invalidateBranchContentCaches(branchPath);
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
35
73
|
/** Detect the current git branch name from the working tree. */
|
|
36
74
|
async function detectCurrentBranch(projectDir) {
|
|
37
75
|
const git = simpleGit({ baseDir: projectDir });
|
|
@@ -140,22 +178,21 @@ async function syncPush(options) {
|
|
|
140
178
|
initialValue: false,
|
|
141
179
|
});
|
|
142
180
|
if (!p.isCancel(shouldAbort) && shouldAbort) {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await invalidateBranchContentCaches(branchPath);
|
|
147
|
-
p.log.success('Merge aborted. Workspace restored to pre-merge state.');
|
|
181
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
182
|
+
p.log.success('Merge aborted. Workspace restored to pre-merge state.');
|
|
183
|
+
}
|
|
148
184
|
}
|
|
149
185
|
}
|
|
150
186
|
return { fileCount: 0 };
|
|
151
187
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
189
|
+
if (editorChanges.length > 0 && !options.force) {
|
|
190
|
+
p.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
191
|
+
for (const file of editorChanges.slice(0, 10)) {
|
|
155
192
|
p.log.warn(` ${file.path}`);
|
|
156
193
|
}
|
|
157
|
-
if (
|
|
158
|
-
p.log.warn(` ... and ${
|
|
194
|
+
if (editorChanges.length > 10) {
|
|
195
|
+
p.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
159
196
|
}
|
|
160
197
|
const confirm = await p.confirm({
|
|
161
198
|
message: 'Continue? Editor changes will be preserved in git history.',
|
|
@@ -166,20 +203,26 @@ async function syncPush(options) {
|
|
|
166
203
|
return { fileCount: 0 };
|
|
167
204
|
}
|
|
168
205
|
}
|
|
169
|
-
// Auto-commit uncommitted workspace changes to preserve in history
|
|
170
|
-
if (status.files.length > 0) {
|
|
171
|
-
await wsGit.add('-A');
|
|
172
|
-
await wsGit.commit('sync: save editor state before push');
|
|
173
|
-
p.log.info('Committed editor changes to history before push');
|
|
174
|
-
}
|
|
175
206
|
p.log.step(`Pushing content to branch workspace: ${branchName}`);
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
207
|
+
// Commit any editor changes (re-read under the lock, so a save landing after
|
|
208
|
+
// the listing above is preserved too), copy working-tree content → workspace
|
|
209
|
+
// content dir, commit, and tag the sync base.
|
|
210
|
+
let fileCount;
|
|
211
|
+
try {
|
|
212
|
+
const result = await pushContentToWorkspace({
|
|
213
|
+
srcContentDir,
|
|
214
|
+
branchPath,
|
|
215
|
+
contentRoot,
|
|
216
|
+
baseTag: SYNC_BASE_TAG,
|
|
217
|
+
saveEditorStateMessage: 'sync: save editor state before push',
|
|
218
|
+
});
|
|
219
|
+
fileCount = result.fileCount;
|
|
220
|
+
if (result.savedEditorState)
|
|
221
|
+
p.log.info('Committed editor changes to history before push');
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
throw lockBusyAsSyncError(err, branchName);
|
|
225
|
+
}
|
|
183
226
|
if (fileCount === 0) {
|
|
184
227
|
p.log.info('Content is already up to date — nothing to push');
|
|
185
228
|
}
|
|
@@ -321,16 +364,23 @@ async function syncBoth(options) {
|
|
|
321
364
|
initialValue: false,
|
|
322
365
|
});
|
|
323
366
|
if (!p.isCancel(shouldAbort) && shouldAbort) {
|
|
324
|
-
await
|
|
325
|
-
|
|
326
|
-
|
|
367
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
368
|
+
p.log.success('Merge aborted. Workspace restored to pre-merge state.');
|
|
369
|
+
}
|
|
327
370
|
}
|
|
328
371
|
}
|
|
329
372
|
return { pushed: 0, pulled: 0 };
|
|
330
373
|
}
|
|
374
|
+
return withWorkspaceLock(branchPath, branchName, () => mergeIntoWorkspace(options, { branchName, branchPath, srcContentDir, contentRoot }));
|
|
375
|
+
}
|
|
376
|
+
/** The locked body of {@link syncBoth}: everything that rewrites the workspace. */
|
|
377
|
+
async function mergeIntoWorkspace(options, ws) {
|
|
378
|
+
const { branchName, branchPath, srcContentDir, contentRoot } = ws;
|
|
379
|
+
const wsGit = simpleGit({ baseDir: branchPath });
|
|
380
|
+
const status = await wsGit.status();
|
|
331
381
|
// Auto-commit uncommitted workspace changes (preserves editor work for the merge)
|
|
332
|
-
if (status.files.
|
|
333
|
-
await wsGit
|
|
382
|
+
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
383
|
+
await stageAllExceptCanopyState(wsGit);
|
|
334
384
|
await wsGit.commit('sync: save editor state before merge');
|
|
335
385
|
p.log.info('Committed editor changes before merge');
|
|
336
386
|
}
|
|
@@ -365,9 +415,9 @@ async function syncBoth(options) {
|
|
|
365
415
|
await wsGit.raw(['branch', '-D', incomingBranch]).catch(() => { });
|
|
366
416
|
throw err;
|
|
367
417
|
}
|
|
368
|
-
await wsGit
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
418
|
+
await stageAllExceptCanopyState(wsGit);
|
|
419
|
+
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
420
|
+
if (incomingFiles.length === 0) {
|
|
371
421
|
await wsGit.checkout(currentBranch);
|
|
372
422
|
await wsGit.raw(['branch', '-D', incomingBranch]);
|
|
373
423
|
p.log.info('No working-tree changes to merge — pulling editor changes only');
|
|
@@ -410,7 +460,7 @@ async function syncBoth(options) {
|
|
|
410
460
|
await wsGit.tag(['-f', SYNC_BASE_TAG]);
|
|
411
461
|
p.log.success('Merged working-tree changes with editor changes');
|
|
412
462
|
const pullResult = await syncPull({ ...options, branch: branchName, force: true });
|
|
413
|
-
return { pushed:
|
|
463
|
+
return { pushed: incomingFiles.length, pulled: pullResult.fileCount };
|
|
414
464
|
}
|
|
415
465
|
finally {
|
|
416
466
|
await invalidateBranchContentCaches(branchPath);
|
|
@@ -428,14 +478,10 @@ async function syncAbort(options) {
|
|
|
428
478
|
return;
|
|
429
479
|
const branchPath = path.join(branchesDir, branchName);
|
|
430
480
|
assertWithinDir(branchPath, branchesDir, '--branch');
|
|
431
|
-
|
|
432
|
-
const status = await wsGit.status();
|
|
433
|
-
if (status.conflicted.length === 0) {
|
|
481
|
+
if (!(await abortWorkspaceMerge(branchPath, branchName))) {
|
|
434
482
|
p.log.info(`Branch workspace "${branchName}" is not in a merge state — nothing to abort.`);
|
|
435
483
|
return;
|
|
436
484
|
}
|
|
437
|
-
await wsGit.merge(['--abort']);
|
|
438
|
-
await invalidateBranchContentCaches(branchPath);
|
|
439
485
|
p.log.success(`Merge aborted in branch workspace "${branchName}". Workspace restored to pre-merge state.`);
|
|
440
486
|
}
|
|
441
487
|
/**
|
|
@@ -73,5 +73,13 @@ COPY --from=builder /app/public ./public
|
|
|
73
73
|
# Lambda's filesystem is read-only outside /tmp. Next may write to .next/cache
|
|
74
74
|
# at runtime; symlink it to /tmp so those writes succeed instead of failing (EROFS).
|
|
75
75
|
RUN rm -rf .next/cache && ln -s /tmp .next/cache
|
|
76
|
+
# The CMS reports this in System health. A build arg, not a Lambda environment
|
|
77
|
+
# variable: a Lambda variable names the infrastructure's commit and would hide
|
|
78
|
+
# exactly the image/infrastructure skew this exists to reveal. Declared after
|
|
79
|
+
# every build layer, so with a warm layer cache a new revision re-runs only the
|
|
80
|
+
# metadata steps from here down.
|
|
81
|
+
ARG CANOPY_SOURCE_SHA=
|
|
82
|
+
ENV CANOPY_SOURCE_SHA=$CANOPY_SOURCE_SHA
|
|
83
|
+
LABEL org.opencontainers.image.revision=$CANOPY_SOURCE_SHA
|
|
76
84
|
ENV PORT=8080 AWS_LWA_PORT=8080 HOSTNAME=0.0.0.0 NODE_ENV=production
|
|
77
85
|
CMD ["node", "server.js"]
|
|
@@ -126,4 +126,8 @@ new CmsStack(app, '{{STACK_NAME}}', {
|
|
|
126
126
|
// point your own existing distribution at it.
|
|
127
127
|
domainName: process.env.CMS_DOMAIN_NAME || undefined,
|
|
128
128
|
hostedZoneDomain: process.env.CMS_HOSTED_ZONE_DOMAIN || undefined,
|
|
129
|
+
|
|
130
|
+
// Optional, and NOT `required()`: the commit the CMS image is built from,
|
|
131
|
+
// shown in System health. Unset, System health reports it as "not set".
|
|
132
|
+
sourceRevision: process.env.CANOPY_SOURCE_SHA || undefined,
|
|
129
133
|
})
|
|
@@ -77,6 +77,8 @@ export interface CmsStackProps extends StackProps {
|
|
|
77
77
|
domainName?: string
|
|
78
78
|
/** Optional Route53 hosted zone domain, e.g. 'example.org'. */
|
|
79
79
|
hostedZoneDomain?: string
|
|
80
|
+
/** Optional. Commit the CMS image is built from; shown in System health. */
|
|
81
|
+
sourceRevision?: string
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export class CmsStack extends Stack {
|
|
@@ -128,6 +130,10 @@ export class CmsStack extends Stack {
|
|
|
128
130
|
// auth. The image's own `next build` stays in dev mode either way --
|
|
129
131
|
// see Dockerfile.cms.
|
|
130
132
|
NEXT_PUBLIC_CANOPY_MODE: 'prod',
|
|
133
|
+
// CDK hashes build args into the asset, so every new commit rebuilds
|
|
134
|
+
// the image, even one changing only the dockerignored infrastructure/
|
|
135
|
+
// -- that is what keeps the revision the CMS reports honest.
|
|
136
|
+
...(props.sourceRevision ? { CANOPY_SOURCE_SHA: props.sourceRevision } : {}),
|
|
131
137
|
},
|
|
132
138
|
}),
|
|
133
139
|
// The one place the CMS image's CPU architecture is decided. CDK derives
|
|
@@ -241,4 +241,6 @@ jobs:
|
|
|
241
241
|
CANOPYCMS_DEPLOYMENT_NAME: ${{ vars.CANOPYCMS_DEPLOYMENT_NAME }}
|
|
242
242
|
CMS_DOMAIN_NAME: ${{ vars.CMS_DOMAIN_NAME }}
|
|
243
243
|
CMS_HOSTED_ZONE_DOMAIN: ${{ vars.CMS_HOSTED_ZONE_DOMAIN }}
|
|
244
|
+
# The commit the CMS image is built from, reported in System health.
|
|
245
|
+
CANOPY_SOURCE_SHA: ${{ github.sha }}
|
|
244
246
|
run: npx cdk deploy {{STACK_NAME}} --require-approval never
|
|
@@ -69,12 +69,13 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
69
69
|
defaultRemoteUrl: z.ZodOptional<z.ZodString>;
|
|
70
70
|
gitBotAuthorName: z.ZodString;
|
|
71
71
|
gitBotAuthorEmail: z.ZodString;
|
|
72
|
+
gitEditedByTrailers: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
gitCoAuthoredByTrailers: z.ZodOptional<z.ZodBoolean>;
|
|
72
74
|
githubTokenEnvVar: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
73
75
|
mode: z.ZodEnum<["prod", "dev"]>;
|
|
74
76
|
deployedAs: z.ZodDefault<z.ZodEnum<["static", "server"]>>;
|
|
75
77
|
allowNetworkRemoteInProd: z.ZodOptional<z.ZodBoolean>;
|
|
76
78
|
settingsBranch: z.ZodOptional<z.ZodString>;
|
|
77
|
-
autoCreateSettingsPR: z.ZodOptional<z.ZodBoolean>;
|
|
78
79
|
deploymentName: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
79
80
|
contentRoot: z.ZodDefault<z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>>>;
|
|
80
81
|
sourceRoot: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
@@ -84,6 +85,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
84
85
|
subtitle: z.ZodOptional<z.ZodString>;
|
|
85
86
|
theme: z.ZodOptional<z.ZodUnknown>;
|
|
86
87
|
previewBase: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
88
|
+
previewPrefix: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
87
89
|
onAccountClick: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodVoid>>;
|
|
88
90
|
onLogoutClick: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodVoid>>;
|
|
89
91
|
AccountComponent: z.ZodOptional<z.ZodType<import("react").ComponentType<{}>, z.ZodTypeDef, import("react").ComponentType<{}>>>;
|
|
@@ -93,6 +95,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
93
95
|
subtitle?: string | undefined;
|
|
94
96
|
theme?: unknown;
|
|
95
97
|
previewBase?: Record<string, string> | undefined;
|
|
98
|
+
previewPrefix?: string | undefined;
|
|
96
99
|
onAccountClick?: ((...args: unknown[]) => void) | undefined;
|
|
97
100
|
onLogoutClick?: ((...args: unknown[]) => void) | undefined;
|
|
98
101
|
AccountComponent?: import("react").ComponentType<{}> | undefined;
|
|
@@ -102,6 +105,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
102
105
|
subtitle?: string | undefined;
|
|
103
106
|
theme?: unknown;
|
|
104
107
|
previewBase?: Record<string, string> | undefined;
|
|
108
|
+
previewPrefix?: string | undefined;
|
|
105
109
|
onAccountClick?: ((...args: unknown[]) => void) | undefined;
|
|
106
110
|
onLogoutClick?: ((...args: unknown[]) => void) | undefined;
|
|
107
111
|
AccountComponent?: import("react").ComponentType<{}> | undefined;
|
|
@@ -152,9 +156,10 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
152
156
|
defaultActiveBranch?: string | undefined;
|
|
153
157
|
defaultRemoteName?: string | undefined;
|
|
154
158
|
defaultRemoteUrl?: string | undefined;
|
|
159
|
+
gitEditedByTrailers?: boolean | undefined;
|
|
160
|
+
gitCoAuthoredByTrailers?: boolean | undefined;
|
|
155
161
|
githubTokenEnvVar?: string | undefined;
|
|
156
162
|
allowNetworkRemoteInProd?: boolean | undefined;
|
|
157
|
-
autoCreateSettingsPR?: boolean | undefined;
|
|
158
163
|
deploymentName?: string | undefined;
|
|
159
164
|
sourceRoot?: string | undefined;
|
|
160
165
|
basePath?: string | undefined;
|
|
@@ -163,6 +168,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
163
168
|
subtitle?: string | undefined;
|
|
164
169
|
theme?: unknown;
|
|
165
170
|
previewBase?: Record<string, string> | undefined;
|
|
171
|
+
previewPrefix?: string | undefined;
|
|
166
172
|
onAccountClick?: ((...args: unknown[]) => void) | undefined;
|
|
167
173
|
onLogoutClick?: ((...args: unknown[]) => void) | undefined;
|
|
168
174
|
AccountComponent?: import("react").ComponentType<{}> | undefined;
|
|
@@ -204,10 +210,11 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
204
210
|
defaultActiveBranch?: string | undefined;
|
|
205
211
|
defaultRemoteName?: string | undefined;
|
|
206
212
|
defaultRemoteUrl?: string | undefined;
|
|
213
|
+
gitEditedByTrailers?: boolean | undefined;
|
|
214
|
+
gitCoAuthoredByTrailers?: boolean | undefined;
|
|
207
215
|
githubTokenEnvVar?: string | undefined;
|
|
208
216
|
deployedAs?: "static" | "server" | undefined;
|
|
209
217
|
allowNetworkRemoteInProd?: boolean | undefined;
|
|
210
|
-
autoCreateSettingsPR?: boolean | undefined;
|
|
211
218
|
deploymentName?: string | undefined;
|
|
212
219
|
contentRoot?: string | undefined;
|
|
213
220
|
sourceRoot?: string | undefined;
|
|
@@ -217,6 +224,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
217
224
|
subtitle?: string | undefined;
|
|
218
225
|
theme?: unknown;
|
|
219
226
|
previewBase?: Record<string, string> | undefined;
|
|
227
|
+
previewPrefix?: string | undefined;
|
|
220
228
|
onAccountClick?: ((...args: unknown[]) => void) | undefined;
|
|
221
229
|
onLogoutClick?: ((...args: unknown[]) => void) | undefined;
|
|
222
230
|
AccountComponent?: import("react").ComponentType<{}> | undefined;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { relativePathSchema } from './collection.js';
|
|
6
6
|
import { mediaSchema } from './media.js';
|
|
7
|
+
import { previewPrefixSchema } from './url.js';
|
|
7
8
|
const defaultBranchAccessSchema = z.enum(['allow', 'deny']).default('deny');
|
|
8
9
|
const defaultPathAccessLevelSchema = z.enum(['allow', 'deny']);
|
|
9
10
|
// Per-level object form: an omitted level stays undefined after parse (no per-field
|
|
@@ -51,6 +52,7 @@ const editorConfigSchema = z.object({
|
|
|
51
52
|
subtitle: z.string().optional(),
|
|
52
53
|
theme: z.unknown().optional(),
|
|
53
54
|
previewBase: z.record(z.string()).optional(),
|
|
55
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
54
56
|
// UI handler functions (runtime only, don't serialize)
|
|
55
57
|
onAccountClick: z.function().returns(z.void()).optional(),
|
|
56
58
|
onLogoutClick: z.function().returns(z.void()).optional(),
|
|
@@ -79,6 +81,9 @@ export const CanopyConfigSchema = z
|
|
|
79
81
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
80
82
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
81
83
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
84
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
85
|
+
gitEditedByTrailers: z.boolean().optional(),
|
|
86
|
+
gitCoAuthoredByTrailers: z.boolean().optional(),
|
|
82
87
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
83
88
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
84
89
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -89,7 +94,6 @@ export const CanopyConfigSchema = z
|
|
|
89
94
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
90
95
|
allowNetworkRemoteInProd: z.boolean().optional(),
|
|
91
96
|
settingsBranch: z.string().optional(),
|
|
92
|
-
autoCreateSettingsPR: z.boolean().optional(),
|
|
93
97
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
94
98
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
95
99
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -81,6 +81,7 @@ const blockFieldSchema = fieldBaseSchema.extend({
|
|
|
81
81
|
const objectFieldSchema = fieldBaseSchema.extend({
|
|
82
82
|
type: z.literal('object'),
|
|
83
83
|
fields: z.array(z.lazy(() => fieldHolder[0])).min(1),
|
|
84
|
+
itemTitleField: z.string().min(1).optional(),
|
|
84
85
|
});
|
|
85
86
|
// Inline group field: visual grouping only, no data nesting
|
|
86
87
|
const inlineGroupFieldSchema = z.object({
|
|
@@ -35,3 +35,5 @@ export declare const uploadTargetUrlSchema: z.ZodEffects<z.ZodString, string, st
|
|
|
35
35
|
* `publicBaseUrl: 'mailto:a@b.c'` and produce `/mailto:a@b.c/assets/…` at render time.
|
|
36
36
|
*/
|
|
37
37
|
export declare const assetMountUrlSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
38
|
+
/** `editor.previewPrefix`, by `uploadTargetUrlSchema`'s rule: `//host` leaves the scheme open. */
|
|
39
|
+
export declare const previewPrefixSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -46,3 +46,10 @@ export const assetMountUrlSchema = z
|
|
|
46
46
|
.refine((value) => isHttpUrlOrSameOriginPath(value, { allowProtocolRelative: true }), {
|
|
47
47
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment',
|
|
48
48
|
});
|
|
49
|
+
/** `editor.previewPrefix`, by `uploadTargetUrlSchema`'s rule: `//host` leaves the scheme open. */
|
|
50
|
+
export const previewPrefixSchema = z
|
|
51
|
+
.string()
|
|
52
|
+
.trim()
|
|
53
|
+
.refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
54
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment',
|
|
55
|
+
});
|
package/dist/config/types.d.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface BlockFieldConfig extends BaseFieldConfig {
|
|
|
132
132
|
export interface ObjectFieldConfig extends BaseFieldConfig {
|
|
133
133
|
type: 'object';
|
|
134
134
|
fields: FieldConfig[];
|
|
135
|
+
/**
|
|
136
|
+
* On a `list: true` object, a direct `string` or `number` child whose value titles each
|
|
137
|
+
* item's card; a blank or non-finite value falls back to "<label> #N".
|
|
138
|
+
*/
|
|
139
|
+
itemTitleField?: string;
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
137
142
|
* Inline group field config: visually groups fields in the editor without creating
|
|
@@ -257,10 +262,25 @@ export interface CanopyEditorConfig {
|
|
|
257
262
|
*
|
|
258
263
|
* Values are **site-relative** and must NOT include the deployment `basePath` — that is applied
|
|
259
264
|
* on top of whatever this yields (see `CanopyConfig.basePath`), so including it here would
|
|
260
|
-
* prefix it twice. An absolute value (`https://…`)
|
|
261
|
-
*
|
|
265
|
+
* prefix it twice. An absolute value (`https://…`) gets only the slug and `?branch=`, with no
|
|
266
|
+
* `basePath`, `previewPrefix` or trailing-slash change: the escape hatch for previewing against a
|
|
267
|
+
* different origin entirely.
|
|
268
|
+
*
|
|
269
|
+
* A site-relative value is a route on the host site, so `previewPrefix` is applied to it too.
|
|
262
270
|
*/
|
|
263
271
|
previewBase?: Record<string, string>;
|
|
272
|
+
/**
|
|
273
|
+
* Where the host mounts the pages the preview pane loads, put in front of every entry's preview
|
|
274
|
+
* URL (`'/preview'` makes `/blog/hello` load `/preview/blog/hello`). Set it when the site's
|
|
275
|
+
* public pages are a static export, which cannot render a requested branch, so the preview
|
|
276
|
+
* must load a route the CMS server renders.
|
|
277
|
+
*
|
|
278
|
+
* A path starting with `/`, without the deployment `basePath` (applied on top, as for
|
|
279
|
+
* `previewBase`), or an absolute `http(s)://` URL, which skips the `basePath`. Pages on another
|
|
280
|
+
* origin accept drafts only with `useCanopyPreview({ editorOrigin })`. An absolute `previewBase`
|
|
281
|
+
* value skips this prefix.
|
|
282
|
+
*/
|
|
283
|
+
previewPrefix?: string;
|
|
264
284
|
onAccountClick?: () => void;
|
|
265
285
|
onLogoutClick?: () => void;
|
|
266
286
|
AccountComponent?: React.ComponentType;
|
|
@@ -346,6 +366,8 @@ export interface CanopyConfig {
|
|
|
346
366
|
defaultRemoteUrl?: DefaultRemoteUrl;
|
|
347
367
|
gitBotAuthorName: GitBotAuthorName;
|
|
348
368
|
gitBotAuthorEmail: GitBotAuthorEmail;
|
|
369
|
+
gitEditedByTrailers?: boolean;
|
|
370
|
+
gitCoAuthoredByTrailers?: boolean;
|
|
349
371
|
githubTokenEnvVar?: GithubTokenEnvVar;
|
|
350
372
|
mode: CanopyOperatingMode;
|
|
351
373
|
/** How this build is deployed — see {@link CanopyConfigInput.deployedAs}. */
|
|
@@ -353,7 +375,6 @@ export interface CanopyConfig {
|
|
|
353
375
|
/** Escape hatch for a prod host with real internet access — see {@link CanopyConfigInput.allowNetworkRemoteInProd}. */
|
|
354
376
|
allowNetworkRemoteInProd?: boolean;
|
|
355
377
|
settingsBranch?: string;
|
|
356
|
-
autoCreateSettingsPR?: boolean;
|
|
357
378
|
deploymentName?: string;
|
|
358
379
|
contentRoot: ContentRoot;
|
|
359
380
|
sourceRoot?: SourceRoot;
|
|
@@ -382,6 +403,10 @@ export interface CanopyConfigInput {
|
|
|
382
403
|
defaultRemoteUrl?: string;
|
|
383
404
|
gitBotAuthorName: string;
|
|
384
405
|
gitBotAuthorEmail: string;
|
|
406
|
+
/** Add `Edited-by: Name (user id)` for the submitter to submit commits. Default true. */
|
|
407
|
+
gitEditedByTrailers?: boolean;
|
|
408
|
+
/** Also add `Co-authored-by: Name <email>`. Default false: emails in public history are public. */
|
|
409
|
+
gitCoAuthoredByTrailers?: boolean;
|
|
385
410
|
githubTokenEnvVar?: string;
|
|
386
411
|
/**
|
|
387
412
|
* Operating mode: 'prod' or 'dev'. Required — no default (SEC-C1). A prod deploy that
|
|
@@ -402,7 +427,6 @@ export interface CanopyConfigInput {
|
|
|
402
427
|
*/
|
|
403
428
|
allowNetworkRemoteInProd?: boolean;
|
|
404
429
|
settingsBranch?: string;
|
|
405
|
-
autoCreateSettingsPR?: boolean;
|
|
406
430
|
deploymentName?: string;
|
|
407
431
|
contentRoot?: string;
|
|
408
432
|
sourceRoot?: string;
|