canopycms 0.0.67 → 0.0.68-int.100
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 +21 -14
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +111 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +122 -0
- package/dist/assets/materialize.js +305 -0
- package/dist/assets/store-local.d.ts +4 -0
- package/dist/assets/store-local.js +11 -0
- package/dist/assets/store-s3.d.ts +10 -1
- package/dist/assets/store-s3.js +55 -3
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +14 -1
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +30 -0
- package/dist/cli/asset-refs.js +168 -0
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +4843 -1870
- package/dist/cli/generate-ai-content.js +1584 -444
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +24 -20
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +11 -0
- package/dist/editor/hooks/useBranchManager.js +80 -2
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +8 -1
- package/dist/editor/preview-bridge.js +15 -2
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +24 -9
- package/dist/server.js +22 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +39 -4
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +2 -1
package/dist/branch-workspace.js
CHANGED
|
@@ -1,121 +1,267 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { BranchPathError, isSettingsBranchName, resolveBranchPath } from './paths/index.js';
|
|
4
|
+
import { buildInitialBranchMetadata, loadBranchContext } from './branch-metadata.js';
|
|
5
|
+
import { BRANCH_META_DIR, BRANCH_META_FILE, readBranchMetadataFile } from './branch-metadata-file.js';
|
|
6
|
+
import { BranchRegistry } from './branch-registry.js';
|
|
7
|
+
import { BranchDirOccupiedError, BranchProvisioningBusyError, CREATE_RESIDUE_MIN_QUIET_MS, classifyFinalDir, removeLeftoverDir, lockBranchName, publishStaging, quietAgeMs, stageBranchWorkspace, } from './branch-provisioning.js';
|
|
8
|
+
import { recordSparseCone, sparseConeFor } from './branch-sparse.js';
|
|
4
9
|
import { readsFromCheckout } from './build-mode.js';
|
|
10
|
+
import { invalidateBranchContentCaches } from './content-index-generation.js';
|
|
5
11
|
import { operatingStrategy } from './operating-mode/index.js';
|
|
6
|
-
import { GitManager } from './git-manager.js';
|
|
7
|
-
import {
|
|
12
|
+
import { GitManager, managedWorkspaceConfig } from './git-manager.js';
|
|
13
|
+
import { getErrorMessage } from './utils/error.js';
|
|
8
14
|
import { resolveBaseBranch } from './utils/git.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
import { canopyLogWarn } from './utils/logger.js';
|
|
16
|
+
import { OccWriteConflictError, writeOccJsonFile } from './utils/occ-json-write.js';
|
|
17
|
+
import { ProvisionLog } from './utils/provision-log.js';
|
|
18
|
+
import { branchProvisioningLockName, PROVISIONING_LOCK_STALE_MS } from './utils/provisioning-lock.js';
|
|
19
|
+
let testHooks = {};
|
|
20
|
+
/** @internal Exported for tests; no argument clears them. */
|
|
21
|
+
export function setProvisioningTestHooks(hooks = {}) {
|
|
22
|
+
testHooks = hooks;
|
|
23
|
+
}
|
|
24
|
+
/** One provisioning per branch path per process; a joiner gets the leader's branch as `exists`. */
|
|
25
|
+
const provisionsInFlight = new Map();
|
|
13
26
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
27
|
+
* Provisions per-branch workspaces under [PROV-1]: a branch's directory appears only by rename,
|
|
28
|
+
* complete with branch.json (branch-provisioning.ts).
|
|
16
29
|
*/
|
|
17
30
|
export class BranchWorkspaceManager {
|
|
18
31
|
constructor(config) {
|
|
19
32
|
this.config = config;
|
|
20
33
|
}
|
|
21
|
-
|
|
22
|
-
return log.timed('workspace', 'ensureGitWorkspace', async () => {
|
|
23
|
-
// One initialization per branch workspace per process.
|
|
24
|
-
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
25
|
-
if (existingLock) {
|
|
26
|
-
await existingLock;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const lockPromise = (async () => {
|
|
30
|
-
// The in-memory lock above only serializes one process, and separate
|
|
31
|
-
// processes can provision the same workspace at once (several Lambda
|
|
32
|
-
// containers on one EFS root; the dev server beside a content-reading
|
|
33
|
-
// script), which without a cross-process lock means both cloning into
|
|
34
|
-
// it: "destination path already exists". initializeWorkspace is
|
|
35
|
-
// idempotent, so the waiter finds the workspace already cloned. A
|
|
36
|
-
// build's content reads never get here — loadOrCreateBranchContext
|
|
37
|
-
// returns the checkout before provisioning.
|
|
38
|
-
let releaseLock;
|
|
39
|
-
try {
|
|
40
|
-
log.debug('workspace', 'Ensuring git workspace', {
|
|
41
|
-
branchName: options.branchName,
|
|
42
|
-
mode: options.mode,
|
|
43
|
-
});
|
|
44
|
-
releaseLock = await acquireProvisioningLock(path.dirname(options.branchRoot), `.${path.basename(options.branchRoot)}.init.lock`);
|
|
45
|
-
await GitManager.initializeWorkspace({
|
|
46
|
-
workspacePath: options.branchRoot,
|
|
47
|
-
branchName: options.branchName,
|
|
48
|
-
mode: options.mode,
|
|
49
|
-
baseBranch: options.baseBranch,
|
|
50
|
-
sourceRoot: this.config.sourceRoot,
|
|
51
|
-
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
52
|
-
remoteUrl: options.remoteUrl,
|
|
53
|
-
remoteName: this.config.defaultRemoteName,
|
|
54
|
-
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
|
|
55
|
-
branchType: 'content',
|
|
56
|
-
gitBotAuthorName: this.config.gitBotAuthorName,
|
|
57
|
-
gitBotAuthorEmail: this.config.gitBotAuthorEmail,
|
|
58
|
-
gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern(),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
// Release the cross-process lock first, then clear the in-memory lock.
|
|
63
|
-
if (releaseLock) {
|
|
64
|
-
try {
|
|
65
|
-
await releaseLock();
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
log.debug('workspace', 'Failed to release workspace-init lock', { err });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
workspaceInitLocks.delete(options.branchRoot);
|
|
72
|
-
}
|
|
73
|
-
})();
|
|
74
|
-
workspaceInitLocks.set(options.branchRoot, lockPromise);
|
|
75
|
-
await lockPromise;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
34
|
+
/** The branch's context, provisioning it first when it does not exist. */
|
|
78
35
|
async openOrCreateBranch(options) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
36
|
+
return (await this.provisionBranch(options)).context;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Provision a branch workspace, or report the one already there.
|
|
40
|
+
*
|
|
41
|
+
* @throws BranchProvisioningBusyError when another process is mid-way through this branch, or
|
|
42
|
+
* what is at its path is too fresh to judge (retriable)
|
|
43
|
+
* @throws BranchDirOccupiedError in prod, when its path holds a repository this deployment did
|
|
44
|
+
* not create
|
|
45
|
+
* @throws BranchMetadataCorruptError when its branch.json does not parse
|
|
46
|
+
*/
|
|
47
|
+
async provisionBranch(options) {
|
|
48
|
+
// resolveBranchPath refuses the reserved prefix; this also refuses an adopter's
|
|
49
|
+
// configured settings branch name, which only the config knows.
|
|
50
|
+
if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
|
|
51
|
+
throw new BranchPathError('Settings branches are not content branches');
|
|
52
|
+
}
|
|
53
|
+
const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
|
|
54
|
+
const paths = {
|
|
55
|
+
branchRoot,
|
|
56
|
+
baseRoot,
|
|
57
|
+
dirName: path.basename(branchRoot),
|
|
58
|
+
safeName,
|
|
59
|
+
};
|
|
60
|
+
const existing = await readBranchMetadataFile(branchRoot);
|
|
61
|
+
if (existing) {
|
|
62
|
+
return { kind: 'exists', context: { branch: existing.branch, branchRoot, baseRoot } };
|
|
63
|
+
}
|
|
64
|
+
const inFlight = provisionsInFlight.get(branchRoot);
|
|
65
|
+
if (inFlight)
|
|
66
|
+
return { kind: 'exists', context: (await inFlight).context };
|
|
67
|
+
const run = this.provisionUnshared(options, paths);
|
|
68
|
+
provisionsInFlight.set(branchRoot, run);
|
|
69
|
+
try {
|
|
70
|
+
return await run;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
provisionsInFlight.delete(branchRoot);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async provisionUnshared(options, paths) {
|
|
77
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
78
|
+
await fs.mkdir(baseRoot, { recursive: true });
|
|
79
|
+
// Resolve the fork point once so the clone and the recorded metadata agree (config value, or
|
|
80
|
+
// dev-mode git HEAD), and the remote before any clone is paid for.
|
|
87
81
|
const baseBranch = await resolveBaseBranch({
|
|
88
82
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
89
|
-
mode,
|
|
83
|
+
mode: options.mode,
|
|
90
84
|
detectFrom: this.config.sourceRoot
|
|
91
85
|
? path.resolve(process.cwd(), this.config.sourceRoot)
|
|
92
86
|
: undefined,
|
|
93
87
|
});
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
const remoteUrl = await GitManager.resolveCloneRemoteUrl({
|
|
89
|
+
mode: options.mode,
|
|
90
|
+
remoteUrl: options.remoteUrl,
|
|
91
|
+
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
98
92
|
baseBranch,
|
|
99
|
-
|
|
93
|
+
sourceRoot: this.config.sourceRoot,
|
|
94
|
+
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
|
|
100
95
|
});
|
|
101
|
-
|
|
102
|
-
// invalidating the registry.
|
|
103
|
-
const metadata = getBranchMetadataFileManager(branchRoot, baseRoot);
|
|
104
|
-
const meta = await metadata.save({
|
|
96
|
+
const metadata = buildInitialBranchMetadata({
|
|
105
97
|
branch: {
|
|
106
|
-
name: safeName,
|
|
107
|
-
title,
|
|
108
|
-
description,
|
|
109
|
-
access,
|
|
110
|
-
createdBy,
|
|
98
|
+
name: paths.safeName,
|
|
99
|
+
title: options.title,
|
|
100
|
+
description: options.description,
|
|
101
|
+
access: options.access,
|
|
102
|
+
createdBy: options.createdBy,
|
|
111
103
|
baseBranch,
|
|
112
104
|
},
|
|
113
105
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
// Advisory and lock-free: refuse early, before paying for a clone, what publish would
|
|
107
|
+
// refuse anyway. Publish re-checks everything under the lock.
|
|
108
|
+
const state = await classifyFinalDir(branchRoot, remoteUrl);
|
|
109
|
+
if (state.kind === 'live' || state.kind === 'corrupt')
|
|
110
|
+
return this.existing(paths);
|
|
111
|
+
if (state.kind === 'protected')
|
|
112
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
113
|
+
if (state.kind === 'foreign') {
|
|
114
|
+
// dev's CLI sync builds a branch workspace by `git init` and real commits, with no
|
|
115
|
+
// remote and no branch.json; it is adopted where it is.
|
|
116
|
+
if (options.mode !== 'dev')
|
|
117
|
+
throw new BranchDirOccupiedError(dirName);
|
|
118
|
+
return this.adoptInPlace(options, paths, baseBranch, metadata);
|
|
119
|
+
}
|
|
120
|
+
if (state.kind === 'residue' && (await this.recentlyActive(paths))) {
|
|
121
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
122
|
+
}
|
|
123
|
+
const sparseCone = sparseConeFor(this.config.contentRoot);
|
|
124
|
+
await recordSparseCone(baseRoot, sparseCone).catch((err) => {
|
|
125
|
+
canopyLogWarn(`[canopy] Could not record the sparse-checkout cone: ${getErrorMessage(err)}`);
|
|
126
|
+
});
|
|
127
|
+
const provisionLog = new ProvisionLog(dirName);
|
|
128
|
+
let outcome;
|
|
129
|
+
try {
|
|
130
|
+
outcome = await this.stageAndPublish(paths, {
|
|
131
|
+
provisionLog,
|
|
132
|
+
baseBranch,
|
|
133
|
+
remoteUrl,
|
|
134
|
+
metadata,
|
|
135
|
+
mode: options.mode,
|
|
136
|
+
sparseCone,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
provisionLog.finish('error');
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
provisionLog.finish(outcome.kind === 'created' ? 'ok' : 'exists');
|
|
144
|
+
return outcome;
|
|
145
|
+
}
|
|
146
|
+
async stageAndPublish(paths, run) {
|
|
147
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
148
|
+
const staged = await stageBranchWorkspace({
|
|
117
149
|
baseRoot,
|
|
118
|
-
|
|
150
|
+
dirName,
|
|
151
|
+
branchName: paths.safeName,
|
|
152
|
+
baseBranch: run.baseBranch,
|
|
153
|
+
remoteUrl: run.remoteUrl,
|
|
154
|
+
clone: {
|
|
155
|
+
remoteName: this.config.defaultRemoteName,
|
|
156
|
+
config: managedWorkspaceConfig(this.config.gitBotAuthorName, this.config.gitBotAuthorEmail),
|
|
157
|
+
},
|
|
158
|
+
gitExcludePattern: operatingStrategy(run.mode).getGitExcludePattern(),
|
|
159
|
+
metadata: run.metadata,
|
|
160
|
+
provisionLog: run.provisionLog,
|
|
161
|
+
sparseCone: run.sparseCone,
|
|
162
|
+
});
|
|
163
|
+
if (staged.kind === 'exists')
|
|
164
|
+
return this.existing(paths);
|
|
165
|
+
let published;
|
|
166
|
+
try {
|
|
167
|
+
await testHooks.beforePublish?.({ stagingPath: staged.stagingPath, finalPath: branchRoot });
|
|
168
|
+
published = await run.provisionLog.step('publish', () => publishStaging({
|
|
169
|
+
baseRoot,
|
|
170
|
+
dirName,
|
|
171
|
+
stagingPath: staged.stagingPath,
|
|
172
|
+
writeId: staged.meta.writeId,
|
|
173
|
+
expectedRemoteUrl: run.remoteUrl,
|
|
174
|
+
inspectBlocked: testHooks.inspectBlocked,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
if (published !== 'published')
|
|
179
|
+
await removeLeftoverDir(staged.stagingPath);
|
|
180
|
+
}
|
|
181
|
+
if (published === 'exists')
|
|
182
|
+
return this.existing(paths);
|
|
183
|
+
await run.provisionLog.step('register', () => register(paths));
|
|
184
|
+
return { kind: 'created', context: { branch: staged.meta.branch, branchRoot, baseRoot } };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The one exception to [PROV-1]: a dev workspace that already holds real work is kept where it
|
|
188
|
+
* is, given branch.json under the branch's lock.
|
|
189
|
+
*/
|
|
190
|
+
async adoptInPlace(options, paths, baseBranch, metadata) {
|
|
191
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
192
|
+
const provisionLog = new ProvisionLog(dirName);
|
|
193
|
+
let created = false;
|
|
194
|
+
try {
|
|
195
|
+
const release = await provisionLog.step('lock', () => lockBranchName(baseRoot, dirName));
|
|
196
|
+
try {
|
|
197
|
+
if (!(await readBranchMetadataFile(branchRoot))) {
|
|
198
|
+
await GitManager.initializeWorkspace({
|
|
199
|
+
workspacePath: branchRoot,
|
|
200
|
+
branchName: paths.safeName,
|
|
201
|
+
mode: options.mode,
|
|
202
|
+
baseBranch,
|
|
203
|
+
sourceRoot: this.config.sourceRoot,
|
|
204
|
+
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
205
|
+
remoteUrl: options.remoteUrl,
|
|
206
|
+
remoteName: this.config.defaultRemoteName,
|
|
207
|
+
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
|
|
208
|
+
branchType: 'content',
|
|
209
|
+
gitBotAuthorName: this.config.gitBotAuthorName,
|
|
210
|
+
gitBotAuthorEmail: this.config.gitBotAuthorEmail,
|
|
211
|
+
gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern(),
|
|
212
|
+
provisionLog,
|
|
213
|
+
});
|
|
214
|
+
created = await provisionLog.step('metadata', () => writeOccJsonFile(path.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE), { ...metadata }, { expectedVersion: null, trailingNewline: true }).then(() => true, (err) => {
|
|
215
|
+
if (err instanceof OccWriteConflictError)
|
|
216
|
+
return false;
|
|
217
|
+
throw err;
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await release().catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
if (created)
|
|
225
|
+
await provisionLog.step('register', () => register(paths));
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
provisionLog.finish('error');
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
provisionLog.finish(created ? 'ok' : 'exists');
|
|
232
|
+
if (!created)
|
|
233
|
+
return this.existing(paths);
|
|
234
|
+
const meta = await readBranchMetadataFile(branchRoot);
|
|
235
|
+
if (!meta)
|
|
236
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
237
|
+
return { kind: 'created', context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
238
|
+
}
|
|
239
|
+
/** Whether residue at the branch path may still belong to a live process. */
|
|
240
|
+
async recentlyActive({ branchRoot, baseRoot, dirName }) {
|
|
241
|
+
const lockAgeMs = await fs.stat(path.join(baseRoot, branchProvisioningLockName(dirName))).then((stat) => Date.now() - stat.mtimeMs, () => Infinity);
|
|
242
|
+
if (lockAgeMs < PROVISIONING_LOCK_STALE_MS)
|
|
243
|
+
return true;
|
|
244
|
+
return (await quietAgeMs(branchRoot)) < CREATE_RESIDUE_MIN_QUIET_MS;
|
|
245
|
+
}
|
|
246
|
+
async existing({ branchRoot, baseRoot, dirName, }) {
|
|
247
|
+
const meta = await readBranchMetadataFile(branchRoot);
|
|
248
|
+
if (!meta)
|
|
249
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
250
|
+
return { kind: 'exists', context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A new branch directory replaces whatever an earlier one of the same name left in this
|
|
255
|
+
* process's caches, and joins the registry. The branch is already published, so a failure here
|
|
256
|
+
* is logged rather than failing the create; the registry's miss backstop covers it.
|
|
257
|
+
*/
|
|
258
|
+
async function register({ branchRoot, baseRoot }) {
|
|
259
|
+
try {
|
|
260
|
+
await invalidateBranchContentCaches(branchRoot);
|
|
261
|
+
await new BranchRegistry(baseRoot).invalidate();
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
canopyLogWarn(`[canopy] Registering new branch ${branchRoot} failed: ${getErrorMessage(err)}`);
|
|
119
265
|
}
|
|
120
266
|
}
|
|
121
267
|
export { loadBranchContext } from './branch-metadata.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the asset URLs a static build references, so a release can make them exist first.
|
|
3
|
+
*
|
|
4
|
+
* The public `/assets/t/` path serves only what is stored, and widths are chosen by site code at
|
|
5
|
+
* render time, so build output is the one place every final URL appears. The adopter contract
|
|
6
|
+
* that follows: every `/assets/t/` URL the site can request must appear as text in its build
|
|
7
|
+
* output (ARCHITECTURE.md, "Why transform by URL directive and materialize at release?").
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/** @internal Written into the scanned directory, so a manifest the adopter builds afterwards covers it. */
|
|
11
|
+
export declare const ASSET_REFS_FILENAME = "canopy-asset-refs.json";
|
|
12
|
+
declare const assetRefsFileSchema: z.ZodObject<{
|
|
13
|
+
version: z.ZodLiteral<1>;
|
|
14
|
+
transforms: z.ZodArray<z.ZodObject<{
|
|
15
|
+
key: z.ZodString;
|
|
16
|
+
routes: z.ZodArray<z.ZodString, "many">;
|
|
17
|
+
files: z.ZodArray<z.ZodString, "many">;
|
|
18
|
+
}, "strict", z.ZodTypeAny, {
|
|
19
|
+
key: string;
|
|
20
|
+
files: string[];
|
|
21
|
+
routes: string[];
|
|
22
|
+
}, {
|
|
23
|
+
key: string;
|
|
24
|
+
files: string[];
|
|
25
|
+
routes: string[];
|
|
26
|
+
}>, "many">;
|
|
27
|
+
statics: z.ZodArray<z.ZodObject<{
|
|
28
|
+
key: z.ZodString;
|
|
29
|
+
routes: z.ZodArray<z.ZodString, "many">;
|
|
30
|
+
files: z.ZodArray<z.ZodString, "many">;
|
|
31
|
+
}, "strict", z.ZodTypeAny, {
|
|
32
|
+
key: string;
|
|
33
|
+
files: string[];
|
|
34
|
+
routes: string[];
|
|
35
|
+
}, {
|
|
36
|
+
key: string;
|
|
37
|
+
files: string[];
|
|
38
|
+
routes: string[];
|
|
39
|
+
}>, "many">;
|
|
40
|
+
}, "strict", z.ZodTypeAny, {
|
|
41
|
+
version: 1;
|
|
42
|
+
transforms: {
|
|
43
|
+
key: string;
|
|
44
|
+
files: string[];
|
|
45
|
+
routes: string[];
|
|
46
|
+
}[];
|
|
47
|
+
statics: {
|
|
48
|
+
key: string;
|
|
49
|
+
files: string[];
|
|
50
|
+
routes: string[];
|
|
51
|
+
}[];
|
|
52
|
+
}, {
|
|
53
|
+
version: 1;
|
|
54
|
+
transforms: {
|
|
55
|
+
key: string;
|
|
56
|
+
files: string[];
|
|
57
|
+
routes: string[];
|
|
58
|
+
}[];
|
|
59
|
+
statics: {
|
|
60
|
+
key: string;
|
|
61
|
+
files: string[];
|
|
62
|
+
routes: string[];
|
|
63
|
+
}[];
|
|
64
|
+
}>;
|
|
65
|
+
export type AssetRefsFile = z.infer<typeof assetRefsFileSchema>;
|
|
66
|
+
/** A URL in the output that no stored object can answer. */
|
|
67
|
+
export interface AssetRefProblem {
|
|
68
|
+
file: string;
|
|
69
|
+
url: string;
|
|
70
|
+
error: string;
|
|
71
|
+
}
|
|
72
|
+
/** The output references URLs that cannot be served from the store; nothing was written. */
|
|
73
|
+
export declare class AssetRefsError extends Error {
|
|
74
|
+
readonly problems: readonly AssetRefProblem[];
|
|
75
|
+
constructor(problems: readonly AssetRefProblem[]);
|
|
76
|
+
}
|
|
77
|
+
export interface CollectAssetRefsResult {
|
|
78
|
+
refs: AssetRefsFile;
|
|
79
|
+
/** Absolute path of the refs file written. */
|
|
80
|
+
filePath: string;
|
|
81
|
+
scannedFiles: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Scan `outDir` for `/assets/t/...` transform URLs and `/assets/{hash32}/...` static URLs and
|
|
85
|
+
* write their stored keys to `outDir/canopy-asset-refs.json`, sorted so the same output always
|
|
86
|
+
* yields the same file. Throws `AssetRefsError`, writing nothing, if any transform URL is not in
|
|
87
|
+
* canonical form or any static URL is malformed: only canonical keys are stored, and the public
|
|
88
|
+
* path is to compute nothing on a miss.
|
|
89
|
+
*/
|
|
90
|
+
export declare function collectAssetRefs(outDir: string): Promise<CollectAssetRefsResult>;
|
|
91
|
+
/** Read and validate a refs file `collectAssetRefs` wrote. */
|
|
92
|
+
export declare function readAssetRefsFile(filePath: string): Promise<AssetRefsFile>;
|
|
93
|
+
export {};
|