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/groups.d.ts
CHANGED
|
@@ -58,29 +58,29 @@ export declare const GROUP_ROUTES: {
|
|
|
58
58
|
description: z.ZodOptional<z.ZodString>;
|
|
59
59
|
members: z.ZodArray<z.ZodString, "many">;
|
|
60
60
|
}, "strip", z.ZodTypeAny, {
|
|
61
|
-
name: string;
|
|
62
61
|
id: string;
|
|
62
|
+
name: string;
|
|
63
63
|
members: string[];
|
|
64
64
|
description?: string | undefined;
|
|
65
65
|
}, {
|
|
66
|
-
name: string;
|
|
67
66
|
id: string;
|
|
67
|
+
name: string;
|
|
68
68
|
members: string[];
|
|
69
69
|
description?: string | undefined;
|
|
70
70
|
}>, "many">;
|
|
71
71
|
expectedContentVersion: z.ZodOptional<z.ZodNumber>;
|
|
72
72
|
}, "strip", z.ZodTypeAny, {
|
|
73
73
|
groups: {
|
|
74
|
-
name: string;
|
|
75
74
|
id: string;
|
|
75
|
+
name: string;
|
|
76
76
|
members: string[];
|
|
77
77
|
description?: string | undefined;
|
|
78
78
|
}[];
|
|
79
79
|
expectedContentVersion?: number | undefined;
|
|
80
80
|
}, {
|
|
81
81
|
groups: {
|
|
82
|
-
name: string;
|
|
83
82
|
id: string;
|
|
83
|
+
name: string;
|
|
84
84
|
members: string[];
|
|
85
85
|
description?: string | undefined;
|
|
86
86
|
}[];
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { ApiContext } from './types.js';
|
|
2
2
|
import type { BranchContext } from '../types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Brings the PR's editor-comment summary
|
|
5
|
-
*
|
|
4
|
+
* Brings both of the PR body's blocks, the editor-comment summary and the review block, up to
|
|
5
|
+
* date after a comment is added, replied to or resolved, or a review is given or withdrawn.
|
|
6
|
+
* Best-effort: a failure is logged and never fails the request.
|
|
6
7
|
*
|
|
7
|
-
* Direct path: read the PR body, replace the
|
|
8
|
-
* DIRECT_SYNC_TIMEOUT_MS so a slow or rate-limited GitHub never holds the
|
|
8
|
+
* Direct path: read the PR body, replace the blocks, write it back, within
|
|
9
|
+
* DIRECT_SYNC_TIMEOUT_MS so a slow or rate-limited GitHub never holds the request.
|
|
9
10
|
* GitHub's PR update takes no expected version; the per-branch lock (prBodyLockKey) only
|
|
10
11
|
* orders this process's writers (docs/concurrency.md).
|
|
11
12
|
*
|
|
12
13
|
* Queued path: one pending task per branch and PR, rendered by the worker from comments.json
|
|
13
|
-
* when it runs, so a burst of
|
|
14
|
-
* call and a pending task reads
|
|
15
|
-
* the change this call skipped; a task already running doesn't count, so a later
|
|
16
|
-
* a run of its own.
|
|
14
|
+
* and branch.json when it runs, so a burst of changes costs one update. The change was written
|
|
15
|
+
* before this call and a pending task reads those files only after it leaves pending, so its
|
|
16
|
+
* run carries the change this call skipped; a task already running doesn't count, so a later
|
|
17
|
+
* change gets a run of its own.
|
|
17
18
|
*/
|
|
18
19
|
export declare function syncPrComments(ctx: ApiContext, context: BranchContext): Promise<void>;
|
|
@@ -5,7 +5,8 @@ import { enqueueTask, listTasks } from '../task-queue/cms-task-queue.js';
|
|
|
5
5
|
import { getTaskQueueDir } from '../task-queue/task-queue-config.js';
|
|
6
6
|
import { withLock } from '../utils/async-mutex.js';
|
|
7
7
|
import { getErrorMessage } from '../utils/error.js';
|
|
8
|
-
|
|
8
|
+
import { shortNameOf } from '../branch-param.js';
|
|
9
|
+
/** A PR whose body blocks are still worth updating: one exists and is not merged or closed. */
|
|
9
10
|
function openPullRequest(context) {
|
|
10
11
|
const { pullRequestNumber, pullRequestState, status } = context.branch;
|
|
11
12
|
if (!pullRequestNumber || status === 'archived')
|
|
@@ -14,22 +15,23 @@ function openPullRequest(context) {
|
|
|
14
15
|
? undefined
|
|
15
16
|
: pullRequestNumber;
|
|
16
17
|
}
|
|
17
|
-
/** How long a
|
|
18
|
+
/** How long a request waits on GitHub for its PR body update, lock wait included. */
|
|
18
19
|
const DIRECT_SYNC_TIMEOUT_MS = 10_000;
|
|
19
20
|
/**
|
|
20
|
-
* Brings the PR's editor-comment summary
|
|
21
|
-
*
|
|
21
|
+
* Brings both of the PR body's blocks, the editor-comment summary and the review block, up to
|
|
22
|
+
* date after a comment is added, replied to or resolved, or a review is given or withdrawn.
|
|
23
|
+
* Best-effort: a failure is logged and never fails the request.
|
|
22
24
|
*
|
|
23
|
-
* Direct path: read the PR body, replace the
|
|
24
|
-
* DIRECT_SYNC_TIMEOUT_MS so a slow or rate-limited GitHub never holds the
|
|
25
|
+
* Direct path: read the PR body, replace the blocks, write it back, within
|
|
26
|
+
* DIRECT_SYNC_TIMEOUT_MS so a slow or rate-limited GitHub never holds the request.
|
|
25
27
|
* GitHub's PR update takes no expected version; the per-branch lock (prBodyLockKey) only
|
|
26
28
|
* orders this process's writers (docs/concurrency.md).
|
|
27
29
|
*
|
|
28
30
|
* Queued path: one pending task per branch and PR, rendered by the worker from comments.json
|
|
29
|
-
* when it runs, so a burst of
|
|
30
|
-
* call and a pending task reads
|
|
31
|
-
* the change this call skipped; a task already running doesn't count, so a later
|
|
32
|
-
* a run of its own.
|
|
31
|
+
* and branch.json when it runs, so a burst of changes costs one update. The change was written
|
|
32
|
+
* before this call and a pending task reads those files only after it leaves pending, so its
|
|
33
|
+
* run carries the change this call skipped; a task already running doesn't count, so a later
|
|
34
|
+
* change gets a run of its own.
|
|
33
35
|
*/
|
|
34
36
|
export async function syncPrComments(ctx, context) {
|
|
35
37
|
const prNumber = openPullRequest(context);
|
|
@@ -38,6 +40,8 @@ export async function syncPrComments(ctx, context) {
|
|
|
38
40
|
if (!clientOperatingStrategy(ctx.services.config.mode).supportsPullRequests())
|
|
39
41
|
return;
|
|
40
42
|
const branch = context.branch.name;
|
|
43
|
+
// The short name, which `?branch=` resolves, for the body's links.
|
|
44
|
+
const shortName = shortNameOf(ctx.services.config, branch);
|
|
41
45
|
const editorUrl = ctx.services.config.editorUrl ?? context.branch.editorUrl;
|
|
42
46
|
try {
|
|
43
47
|
const { githubService } = ctx.services;
|
|
@@ -48,9 +52,10 @@ export async function syncPrComments(ctx, context) {
|
|
|
48
52
|
return;
|
|
49
53
|
const body = await renderPrCommentsBody({
|
|
50
54
|
branchRoot: context.branchRoot,
|
|
51
|
-
branch,
|
|
55
|
+
branch: shortName,
|
|
52
56
|
editorUrl,
|
|
53
57
|
body: pr.body,
|
|
58
|
+
warn: (message) => console.warn(`CanopyCMS: ${message}`),
|
|
54
59
|
});
|
|
55
60
|
if (body !== undefined)
|
|
56
61
|
await githubService.updatePullRequest(prNumber, { body }, signal);
|
|
@@ -66,11 +71,16 @@ export async function syncPrComments(ctx, context) {
|
|
|
66
71
|
return;
|
|
67
72
|
await enqueueTask(taskDir, {
|
|
68
73
|
action: SYNC_PR_COMMENTS_ACTION,
|
|
69
|
-
payload: {
|
|
74
|
+
payload: {
|
|
75
|
+
branch,
|
|
76
|
+
pullRequestNumber: prNumber,
|
|
77
|
+
...(editorUrl ? { editorUrl } : {}),
|
|
78
|
+
editorBranch: shortName,
|
|
79
|
+
},
|
|
70
80
|
});
|
|
71
81
|
}
|
|
72
82
|
catch (err) {
|
|
73
|
-
console.warn(`CanopyCMS: Could not update the editor comments on PR #${prNumber} for ${branch}:`, isApiTimeoutError(err)
|
|
83
|
+
console.warn(`CanopyCMS: Could not update the editor comments or review state on PR #${prNumber} for ${branch}:`, isApiTimeoutError(err)
|
|
74
84
|
? `GitHub did not answer within ${DIRECT_SYNC_TIMEOUT_MS / 1000} s`
|
|
75
85
|
: getErrorMessage(err));
|
|
76
86
|
}
|
package/dist/api/routes.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* `http-reaches-api-only-via-routes` dependency-cruiser rule holds this one.
|
|
6
6
|
*/
|
|
7
7
|
import { BRANCH_ROUTES } from './branch.js';
|
|
8
|
+
import { BRANCH_CHANGES_ROUTES } from './branch-changes.js';
|
|
9
|
+
import { BRANCH_ENTRY_CHANGE_ROUTES } from './branch-entry-changes.js';
|
|
8
10
|
import { WORKFLOW_ROUTES } from './branch-status.js';
|
|
9
11
|
import { COMMENT_ROUTES } from './comments.js';
|
|
10
12
|
import { CONTENT_ROUTES } from './content.js';
|
|
@@ -24,6 +26,8 @@ import { ADMIN_ROUTES } from './admin.js';
|
|
|
24
26
|
export function buildCanopyRoutes() {
|
|
25
27
|
return [
|
|
26
28
|
...Object.values(BRANCH_ROUTES),
|
|
29
|
+
...Object.values(BRANCH_CHANGES_ROUTES),
|
|
30
|
+
...Object.values(BRANCH_ENTRY_CHANGE_ROUTES),
|
|
27
31
|
...Object.values(WORKFLOW_ROUTES),
|
|
28
32
|
...Object.values(COMMENT_ROUTES),
|
|
29
33
|
...Object.values(CONTENT_ROUTES),
|
package/dist/api/types.d.ts
CHANGED
|
@@ -65,5 +65,7 @@ export interface ApiResponse<TData = unknown> {
|
|
|
65
65
|
* 409, see `SettingsVersionConflictError`); nothing was saved, and a reload shows the other edit.
|
|
66
66
|
* The other settings 409, a busy file lock, carries no code. `BRANCH_SYNCING`: an entry save was
|
|
67
67
|
* refused because the branch is mid-rebase (a 409); nothing was saved, and a retry soon can pass.
|
|
68
|
+
* `BRANCH_NAME_REFUSED`: a branch create was refused for the name it gave (a 400 or 409), so only
|
|
69
|
+
* another name can pass; no other create failure carries it.
|
|
68
70
|
*/
|
|
69
|
-
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING';
|
|
71
|
+
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED';
|
|
@@ -14,21 +14,21 @@ export declare const GroupsFileSchema: z.ZodObject<{
|
|
|
14
14
|
description: z.ZodOptional<z.ZodString>;
|
|
15
15
|
members: z.ZodArray<z.ZodType<string, z.ZodTypeDef, string>, "many">;
|
|
16
16
|
}, "strip", z.ZodTypeAny, {
|
|
17
|
-
name: string;
|
|
18
17
|
id: string;
|
|
18
|
+
name: string;
|
|
19
19
|
members: string[];
|
|
20
20
|
description?: string | undefined;
|
|
21
21
|
}, {
|
|
22
|
-
name: string;
|
|
23
22
|
id: string;
|
|
23
|
+
name: string;
|
|
24
24
|
members: string[];
|
|
25
25
|
description?: string | undefined;
|
|
26
26
|
}>, "many">;
|
|
27
27
|
}, "strip", z.ZodTypeAny, {
|
|
28
28
|
updatedAt: string;
|
|
29
29
|
groups: {
|
|
30
|
-
name: string;
|
|
31
30
|
id: string;
|
|
31
|
+
name: string;
|
|
32
32
|
members: string[];
|
|
33
33
|
description?: string | undefined;
|
|
34
34
|
}[];
|
|
@@ -38,8 +38,8 @@ export declare const GroupsFileSchema: z.ZodObject<{
|
|
|
38
38
|
}, {
|
|
39
39
|
updatedAt: string;
|
|
40
40
|
groups: {
|
|
41
|
-
name: string;
|
|
42
41
|
id: string;
|
|
42
|
+
name: string;
|
|
43
43
|
members: string[];
|
|
44
44
|
description?: string | undefined;
|
|
45
45
|
}[];
|
|
@@ -52,9 +52,9 @@ export interface BranchWriteProtection extends BranchProtection {
|
|
|
52
52
|
/**
|
|
53
53
|
* Whether `branchName` is the protected base branch for `config`.
|
|
54
54
|
*
|
|
55
|
-
* The comparison is sanitization-aware:
|
|
56
|
-
*
|
|
57
|
-
* `
|
|
55
|
+
* The comparison is sanitization-aware: both sides go through `sanitizeBranchName`
|
|
56
|
+
* first, the form a workspace directory takes, so a base workspace whose recorded
|
|
57
|
+
* name is sanitized still matches the raw git name `config.defaultBaseBranch` holds.
|
|
58
58
|
*
|
|
59
59
|
* `recordedBaseBranch` (a branch's own recorded fork point) is a second, purely
|
|
60
60
|
* additive clause: a branch whose fork point equals its own name IS a base
|
|
@@ -15,9 +15,9 @@ import { baseBranchOf } from '../utils/base-branch.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* Whether `branchName` is the protected base branch for `config`.
|
|
17
17
|
*
|
|
18
|
-
* The comparison is sanitization-aware:
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
18
|
+
* The comparison is sanitization-aware: both sides go through `sanitizeBranchName`
|
|
19
|
+
* first, the form a workspace directory takes, so a base workspace whose recorded
|
|
20
|
+
* name is sanitized still matches the raw git name `config.defaultBaseBranch` holds.
|
|
21
21
|
*
|
|
22
22
|
* `recordedBaseBranch` (a branch's own recorded fork point) is a second, purely
|
|
23
23
|
* additive clause: a branch whose fork point equals its own name IS a base
|
|
@@ -85,6 +85,34 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
85
85
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
86
86
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
87
87
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
88
|
+
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
89
|
+
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
90
|
+
by: z.ZodObject<{
|
|
91
|
+
userId: z.ZodString;
|
|
92
|
+
name: z.ZodOptional<z.ZodString>;
|
|
93
|
+
}, "strip", z.ZodTypeAny, {
|
|
94
|
+
userId: string;
|
|
95
|
+
name?: string | undefined;
|
|
96
|
+
}, {
|
|
97
|
+
userId: string;
|
|
98
|
+
name?: string | undefined;
|
|
99
|
+
}>;
|
|
100
|
+
at: z.ZodString;
|
|
101
|
+
}, "strip", z.ZodTypeAny, {
|
|
102
|
+
at: string;
|
|
103
|
+
state: "approved" | "changes-requested";
|
|
104
|
+
by: {
|
|
105
|
+
userId: string;
|
|
106
|
+
name?: string | undefined;
|
|
107
|
+
};
|
|
108
|
+
}, {
|
|
109
|
+
at: string;
|
|
110
|
+
state: "approved" | "changes-requested";
|
|
111
|
+
by: {
|
|
112
|
+
userId: string;
|
|
113
|
+
name?: string | undefined;
|
|
114
|
+
};
|
|
115
|
+
}>>>;
|
|
88
116
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
89
117
|
name: z.ZodString;
|
|
90
118
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -150,6 +178,34 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
150
178
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
151
179
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
152
180
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
181
|
+
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
182
|
+
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
183
|
+
by: z.ZodObject<{
|
|
184
|
+
userId: z.ZodString;
|
|
185
|
+
name: z.ZodOptional<z.ZodString>;
|
|
186
|
+
}, "strip", z.ZodTypeAny, {
|
|
187
|
+
userId: string;
|
|
188
|
+
name?: string | undefined;
|
|
189
|
+
}, {
|
|
190
|
+
userId: string;
|
|
191
|
+
name?: string | undefined;
|
|
192
|
+
}>;
|
|
193
|
+
at: z.ZodString;
|
|
194
|
+
}, "strip", z.ZodTypeAny, {
|
|
195
|
+
at: string;
|
|
196
|
+
state: "approved" | "changes-requested";
|
|
197
|
+
by: {
|
|
198
|
+
userId: string;
|
|
199
|
+
name?: string | undefined;
|
|
200
|
+
};
|
|
201
|
+
}, {
|
|
202
|
+
at: string;
|
|
203
|
+
state: "approved" | "changes-requested";
|
|
204
|
+
by: {
|
|
205
|
+
userId: string;
|
|
206
|
+
name?: string | undefined;
|
|
207
|
+
};
|
|
208
|
+
}>>>;
|
|
153
209
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
154
210
|
name: z.ZodString;
|
|
155
211
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -215,6 +271,34 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
215
271
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
216
272
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
217
273
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
274
|
+
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
275
|
+
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
276
|
+
by: z.ZodObject<{
|
|
277
|
+
userId: z.ZodString;
|
|
278
|
+
name: z.ZodOptional<z.ZodString>;
|
|
279
|
+
}, "strip", z.ZodTypeAny, {
|
|
280
|
+
userId: string;
|
|
281
|
+
name?: string | undefined;
|
|
282
|
+
}, {
|
|
283
|
+
userId: string;
|
|
284
|
+
name?: string | undefined;
|
|
285
|
+
}>;
|
|
286
|
+
at: z.ZodString;
|
|
287
|
+
}, "strip", z.ZodTypeAny, {
|
|
288
|
+
at: string;
|
|
289
|
+
state: "approved" | "changes-requested";
|
|
290
|
+
by: {
|
|
291
|
+
userId: string;
|
|
292
|
+
name?: string | undefined;
|
|
293
|
+
};
|
|
294
|
+
}, {
|
|
295
|
+
at: string;
|
|
296
|
+
state: "approved" | "changes-requested";
|
|
297
|
+
by: {
|
|
298
|
+
userId: string;
|
|
299
|
+
name?: string | undefined;
|
|
300
|
+
};
|
|
301
|
+
}>>>;
|
|
218
302
|
}, z.ZodTypeAny, "passthrough">>;
|
|
219
303
|
/**
|
|
220
304
|
* Read, parse and validate `branch.json`, with no locking, no OCC and no side
|
|
@@ -84,6 +84,15 @@ const branchMetadataShape = {
|
|
|
84
84
|
syncFailureReason: z.string().optional(),
|
|
85
85
|
editors: z.array(z.string()).optional(),
|
|
86
86
|
uncommittedEditors: z.array(z.string()).optional(),
|
|
87
|
+
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
88
|
+
review: z
|
|
89
|
+
.object({
|
|
90
|
+
state: z.enum(['approved', 'changes-requested']),
|
|
91
|
+
by: z.object({ userId: z.string(), name: z.string().optional() }),
|
|
92
|
+
at: z.string(),
|
|
93
|
+
})
|
|
94
|
+
.optional()
|
|
95
|
+
.catch(undefined),
|
|
87
96
|
};
|
|
88
97
|
/** The `branch` object, also embedded in the registry snapshot (branch-registry.ts). */
|
|
89
98
|
export const branchMetadataSchema = z.object(branchMetadataShape).passthrough();
|
|
@@ -124,11 +124,11 @@ export interface BranchMetadataUpdate {
|
|
|
124
124
|
* worker's merge-poll (CmsWorker.pollMergeState) and the manual markAsMerged
|
|
125
125
|
* API (api/branch-merge.ts), so both produce identical metadata.
|
|
126
126
|
*
|
|
127
|
-
* Deliberately omits pullRequestNumber/pullRequestUrl: save() keeps
|
|
128
|
-
* values for fields the incoming update omits, so the
|
|
129
|
-
* URL survive untouched.
|
|
127
|
+
* Deliberately omits `name` and pullRequestNumber/pullRequestUrl: save() keeps
|
|
128
|
+
* existing values for fields the incoming update omits, so the branch's ref and
|
|
129
|
+
* its recorded PR number and URL survive untouched.
|
|
130
130
|
*/
|
|
131
|
-
export declare function buildMergedBranchUpdate(
|
|
131
|
+
export declare function buildMergedBranchUpdate(now?: Date): NonNullable<BranchMetadataUpdate['branch']>;
|
|
132
132
|
/** {@link BranchMetadataFileManager.get} as a function. */
|
|
133
133
|
export declare const getBranchMetadataFileManager: (branchRoot: string, baseRoot: string, options?: {
|
|
134
134
|
settleMs?: number;
|
package/dist/branch-metadata.js
CHANGED
|
@@ -272,13 +272,12 @@ export function buildInitialBranchMetadata(incoming, now) {
|
|
|
272
272
|
* worker's merge-poll (CmsWorker.pollMergeState) and the manual markAsMerged
|
|
273
273
|
* API (api/branch-merge.ts), so both produce identical metadata.
|
|
274
274
|
*
|
|
275
|
-
* Deliberately omits pullRequestNumber/pullRequestUrl: save() keeps
|
|
276
|
-
* values for fields the incoming update omits, so the
|
|
277
|
-
* URL survive untouched.
|
|
275
|
+
* Deliberately omits `name` and pullRequestNumber/pullRequestUrl: save() keeps
|
|
276
|
+
* existing values for fields the incoming update omits, so the branch's ref and
|
|
277
|
+
* its recorded PR number and URL survive untouched.
|
|
278
278
|
*/
|
|
279
|
-
export function buildMergedBranchUpdate(
|
|
279
|
+
export function buildMergedBranchUpdate(now = new Date()) {
|
|
280
280
|
return {
|
|
281
|
-
name: branchName,
|
|
282
281
|
status: 'archived',
|
|
283
282
|
pullRequestState: 'merged',
|
|
284
283
|
mergedAt: now.toISOString(),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CanopyConfig } from './config/index.js';
|
|
2
|
+
import type { BranchContext } from './types.js';
|
|
3
|
+
export type BranchParamConfig = Pick<CanopyConfig, 'mode' | 'defaultBaseBranch' | 'defaultActiveBranch' | 'branchNamespace' | 'deploymentName'>;
|
|
4
|
+
/**
|
|
5
|
+
* The name pull request links and messages give the branch whose ref is `name`: its short name
|
|
6
|
+
* under this CMS's namespace, or the full ref when the short name is the base or active
|
|
7
|
+
* branch's, which the resolver would answer instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shortNameOf(config: BranchParamConfig, name: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* The workspace `param` names, or null. A `param` in the exact form (`exactBranchSegment`,
|
|
12
|
+
* `~canopycms~prod~spring`) names only the branch whose branch.json `name` it decodes to, and a
|
|
13
|
+
* malformed one names none. Any other `param`, in order:
|
|
14
|
+
* 1. the base or configured active branch, exactly, so no namespace branch ever shadows it;
|
|
15
|
+
* 2. the namespace branch whose short name is `param` (which holds no `/`), when its branch.json
|
|
16
|
+
* `name` is that ref;
|
|
17
|
+
* 3. a branch outside the namespace by its full name, when its `name` is `param`. A namespace
|
|
18
|
+
* branch's full ref resolves here too. A branch outside the namespace that step 2 shadows
|
|
19
|
+
* opens only by the exact form.
|
|
20
|
+
* Throws what loading throws for a name that can name no workspace (`namesNoWorkspace`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveBranchParam(param: string, config: BranchParamConfig, options?: {
|
|
23
|
+
basePathOverride?: string;
|
|
24
|
+
}): Promise<BranchContext | null>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way a branch named in a request, by a `/:branch` segment or `?branch=`, becomes its
|
|
3
|
+
* workspace. A branch has three names: its ref (`canopycms/prod/spring`), which git and
|
|
4
|
+
* branch.json's `name` hold; its key (`sanitizeBranchName(ref)`), the workspace directory and
|
|
5
|
+
* registry key; and its short name (`spring`, `branchShortName`), which pull request links carry
|
|
6
|
+
* and requests may carry in place of the ref.
|
|
7
|
+
*/
|
|
8
|
+
import { loadBranchContext } from './branch-metadata.js';
|
|
9
|
+
import { operatingStrategy } from './operating-mode/index.js';
|
|
10
|
+
import { namesNoWorkspace } from './paths/branch.js';
|
|
11
|
+
import { branchShortName } from './paths/branch-name.js';
|
|
12
|
+
import { exactBranchName, isExactBranchForm } from './paths/validation.js';
|
|
13
|
+
import { BaseBranchUnresolvedError, baseBranchOf } from './utils/base-branch.js';
|
|
14
|
+
/**
|
|
15
|
+
* The namespace this CMS creates branches in, or null when its template is invalid: creation
|
|
16
|
+
* then refuses, and System health names why.
|
|
17
|
+
*/
|
|
18
|
+
function cmsBranchNamespace(config) {
|
|
19
|
+
try {
|
|
20
|
+
return operatingStrategy(config.mode).getBranchNamespace(config);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Whether `param` is the base or configured active branch, which resolve before any other. */
|
|
27
|
+
function namesBaseOrActive(param, config) {
|
|
28
|
+
let base;
|
|
29
|
+
try {
|
|
30
|
+
base = baseBranchOf(config);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof BaseBranchUnresolvedError)
|
|
34
|
+
return false;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
return param === base || param === (config.defaultActiveBranch ?? base);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The name pull request links and messages give the branch whose ref is `name`: its short name
|
|
41
|
+
* under this CMS's namespace, or the full ref when the short name is the base or active
|
|
42
|
+
* branch's, which the resolver would answer instead.
|
|
43
|
+
*/
|
|
44
|
+
export function shortNameOf(config, name) {
|
|
45
|
+
const short = branchShortName(name, cmsBranchNamespace(config));
|
|
46
|
+
return short !== name && namesBaseOrActive(short, config) ? name : short;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The workspace `param` names, or null. A `param` in the exact form (`exactBranchSegment`,
|
|
50
|
+
* `~canopycms~prod~spring`) names only the branch whose branch.json `name` it decodes to, and a
|
|
51
|
+
* malformed one names none. Any other `param`, in order:
|
|
52
|
+
* 1. the base or configured active branch, exactly, so no namespace branch ever shadows it;
|
|
53
|
+
* 2. the namespace branch whose short name is `param` (which holds no `/`), when its branch.json
|
|
54
|
+
* `name` is that ref;
|
|
55
|
+
* 3. a branch outside the namespace by its full name, when its `name` is `param`. A namespace
|
|
56
|
+
* branch's full ref resolves here too. A branch outside the namespace that step 2 shadows
|
|
57
|
+
* opens only by the exact form.
|
|
58
|
+
* Throws what loading throws for a name that can name no workspace (`namesNoWorkspace`).
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveBranchParam(param, config, options = {}) {
|
|
61
|
+
const load = (branchName) => loadBranchContext({ branchName, mode: config.mode, basePathOverride: options.basePathOverride });
|
|
62
|
+
if (isExactBranchForm(param)) {
|
|
63
|
+
const exact = exactBranchName(param);
|
|
64
|
+
if (exact === null)
|
|
65
|
+
return null;
|
|
66
|
+
const named = await load(exact);
|
|
67
|
+
return named?.branch.name === exact ? named : null;
|
|
68
|
+
}
|
|
69
|
+
if (namesBaseOrActive(param, config))
|
|
70
|
+
return load(param);
|
|
71
|
+
const namespace = cmsBranchNamespace(config);
|
|
72
|
+
// A short name holding a `/` is a full name (`branchShortName`), so a `param` with one names no
|
|
73
|
+
// namespace branch by the part after the namespace.
|
|
74
|
+
if (namespace !== null && !param.includes('/')) {
|
|
75
|
+
const ref = `${namespace}${param}`;
|
|
76
|
+
// A ref past a filename's length can still leave `param` naming a branch outside it.
|
|
77
|
+
const inNamespace = await load(ref).catch((err) => {
|
|
78
|
+
if (namesNoWorkspace(err))
|
|
79
|
+
return null;
|
|
80
|
+
throw err;
|
|
81
|
+
});
|
|
82
|
+
if (inNamespace?.branch.name === ref)
|
|
83
|
+
return inNamespace;
|
|
84
|
+
}
|
|
85
|
+
const outside = await load(param);
|
|
86
|
+
return outside?.branch.name === param ? outside : null;
|
|
87
|
+
}
|
|
@@ -5,11 +5,12 @@ import type { ProvisionLog } from './utils/provision-log.js';
|
|
|
5
5
|
export declare const CREATE_RESIDUE_MIN_QUIET_MS = 60000;
|
|
6
6
|
/**
|
|
7
7
|
* Retriable: another process is setting this branch up, or what is at its path is too recently
|
|
8
|
-
* touched (or under an admin repair) to judge. Callers answer 503.
|
|
8
|
+
* touched (or under an admin repair) to judge. Callers answer 503. The message names the branch
|
|
9
|
+
* by `shortName`, which an editor knows it by, not by its workspace directory.
|
|
9
10
|
*/
|
|
10
11
|
export declare class BranchProvisioningBusyError extends Error {
|
|
11
12
|
readonly dirName: string;
|
|
12
|
-
constructor(dirName: string, message?: string);
|
|
13
|
+
constructor(dirName: string, shortName?: string, message?: string);
|
|
13
14
|
}
|
|
14
15
|
/** The branch's path holds a git repository this deployment did not create (prod only). */
|
|
15
16
|
export declare class BranchDirOccupiedError extends Error {
|
|
@@ -130,12 +131,14 @@ export declare function stageBranchWorkspace(options: StageBranchOptions): Promi
|
|
|
130
131
|
* The branch's provisioning lock with a bounded wait; contention is
|
|
131
132
|
* {@link BranchProvisioningBusyError}.
|
|
132
133
|
*/
|
|
133
|
-
export declare function lockBranchName(baseRoot: string, dirName: string): Promise<() => Promise<void>>;
|
|
134
|
+
export declare function lockBranchName(baseRoot: string, dirName: string, shortName?: string): Promise<() => Promise<void>>;
|
|
134
135
|
/** What blocks a refused publish rename, as {@link publishStaging} sees it. */
|
|
135
136
|
export type BlockingKind = 'live' | 'protected' | 'residue';
|
|
136
137
|
export interface PublishOptions {
|
|
137
138
|
baseRoot: string;
|
|
138
139
|
dirName: string;
|
|
140
|
+
/** What a busy answer calls the branch; defaults to `dirName`. */
|
|
141
|
+
shortName?: string;
|
|
139
142
|
stagingPath: string;
|
|
140
143
|
/** branch.json's `writeId` in the staged workspace: proof the rename was ours. */
|
|
141
144
|
writeId: string | undefined;
|
|
@@ -45,10 +45,11 @@ const DELETING_MAX_AGE_MS = 10 * 60_000;
|
|
|
45
45
|
const SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 60_000;
|
|
46
46
|
/**
|
|
47
47
|
* Retriable: another process is setting this branch up, or what is at its path is too recently
|
|
48
|
-
* touched (or under an admin repair) to judge. Callers answer 503.
|
|
48
|
+
* touched (or under an admin repair) to judge. Callers answer 503. The message names the branch
|
|
49
|
+
* by `shortName`, which an editor knows it by, not by its workspace directory.
|
|
49
50
|
*/
|
|
50
51
|
export class BranchProvisioningBusyError extends Error {
|
|
51
|
-
constructor(dirName, message = `Branch '${
|
|
52
|
+
constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
|
|
52
53
|
super(message);
|
|
53
54
|
this.dirName = dirName;
|
|
54
55
|
this.name = 'BranchProvisioningBusyError';
|
|
@@ -334,13 +335,14 @@ export async function stageBranchWorkspace(options) {
|
|
|
334
335
|
* The branch's provisioning lock with a bounded wait; contention is
|
|
335
336
|
* {@link BranchProvisioningBusyError}.
|
|
336
337
|
*/
|
|
337
|
-
export async function lockBranchName(baseRoot, dirName) {
|
|
338
|
+
export async function lockBranchName(baseRoot, dirName, shortName = dirName) {
|
|
338
339
|
try {
|
|
339
340
|
return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
|
|
340
341
|
}
|
|
341
342
|
catch (err) {
|
|
342
|
-
if (isNodeError(err) && err.code === 'ELOCKED')
|
|
343
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
343
|
+
if (isNodeError(err) && err.code === 'ELOCKED') {
|
|
344
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
345
|
+
}
|
|
344
346
|
throw err;
|
|
345
347
|
}
|
|
346
348
|
}
|
|
@@ -363,9 +365,9 @@ const RENAME_BLOCKED = new Set(['EEXIST', 'ENOTEMPTY', 'ENOTDIR']);
|
|
|
363
365
|
* `writeId`. The caller removes the staging directory on any outcome but `published`.
|
|
364
366
|
*/
|
|
365
367
|
export async function publishStaging(options) {
|
|
366
|
-
const { baseRoot, dirName, stagingPath } = options;
|
|
368
|
+
const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
|
|
367
369
|
const finalPath = path.join(baseRoot, dirName);
|
|
368
|
-
const release = await lockBranchName(baseRoot, dirName);
|
|
370
|
+
const release = await lockBranchName(baseRoot, dirName, shortName);
|
|
369
371
|
try {
|
|
370
372
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
371
373
|
try {
|
|
@@ -388,7 +390,7 @@ export async function publishStaging(options) {
|
|
|
388
390
|
if (blocking === 'live')
|
|
389
391
|
return 'exists';
|
|
390
392
|
if (blocking === 'protected')
|
|
391
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
393
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
392
394
|
const result = await quarantineResidueAt(baseRoot, dirName, {
|
|
393
395
|
minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
|
|
394
396
|
expectedRemoteUrl: options.expectedRemoteUrl,
|
|
@@ -403,9 +405,9 @@ export async function publishStaging(options) {
|
|
|
403
405
|
}
|
|
404
406
|
if (result.reason === 'foreign')
|
|
405
407
|
throw new BranchDirOccupiedError(dirName);
|
|
406
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
408
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
407
409
|
}
|
|
408
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
410
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
409
411
|
}
|
|
410
412
|
finally {
|
|
411
413
|
await release().catch((err) => {
|
|
@@ -56,6 +56,12 @@ export declare class BranchRegistry {
|
|
|
56
56
|
* genuinely-absent branch costs at most one extra scan per window.
|
|
57
57
|
*/
|
|
58
58
|
get(name: string): Promise<BranchContext | undefined>;
|
|
59
|
+
/**
|
|
60
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
61
|
+
* whatever its name, with {@link get}'s backstop.
|
|
62
|
+
*/
|
|
63
|
+
getByKey(key: string): Promise<BranchContext | undefined>;
|
|
64
|
+
private find;
|
|
59
65
|
private shouldForceRefresh;
|
|
60
66
|
/**
|
|
61
67
|
* Marks the cache stale for every process sharing this root by bumping the
|
package/dist/branch-registry.js
CHANGED
|
@@ -136,14 +136,23 @@ export class BranchRegistry {
|
|
|
136
136
|
* genuinely-absent branch costs at most one extra scan per window.
|
|
137
137
|
*/
|
|
138
138
|
async get(name) {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
return this.find((b) => b.branch.name === name);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
143
|
+
* whatever its name, with {@link get}'s backstop.
|
|
144
|
+
*/
|
|
145
|
+
async getByKey(key) {
|
|
146
|
+
return this.find((b) => path.basename(b.branchRoot) === key);
|
|
147
|
+
}
|
|
148
|
+
async find(matches) {
|
|
149
|
+
const found = (await this.list()).find(matches);
|
|
141
150
|
if (found)
|
|
142
151
|
return found;
|
|
143
152
|
if (!this.shouldForceRefresh())
|
|
144
153
|
return undefined;
|
|
145
154
|
const refreshed = await this.regenerate();
|
|
146
|
-
return refreshed.find(
|
|
155
|
+
return refreshed.find(matches);
|
|
147
156
|
}
|
|
148
157
|
shouldForceRefresh() {
|
|
149
158
|
const now = Date.now();
|