canopycms 0.12.1 → 0.13.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +211 -49
  2. package/dist/api/branch-review.d.ts +20 -2
  3. package/dist/api/branch-review.js +112 -23
  4. package/dist/api/branch-status.d.ts +11 -2
  5. package/dist/api/branch-status.js +3 -2
  6. package/dist/api/branch-update-from-base.js +57 -6
  7. package/dist/api/branch.d.ts +5 -0
  8. package/dist/api/branch.js +27 -2
  9. package/dist/api/client.d.ts +2 -2
  10. package/dist/api/client.js +2 -2
  11. package/dist/api/content.js +1 -1
  12. package/dist/api/merge-conflicts.d.ts +10 -1
  13. package/dist/api/merge-conflicts.js +42 -7
  14. package/dist/api/types.d.ts +3 -2
  15. package/dist/base-update.d.ts +6 -0
  16. package/dist/branch-metadata-file.d.ts +15 -0
  17. package/dist/branch-metadata-file.js +5 -1
  18. package/dist/cli/cli.js +238 -108
  19. package/dist/cli/generate-ai-content.js +20 -6
  20. package/dist/cli/github-app-manifest.d.ts +4 -1
  21. package/dist/cli/github-app-manifest.js +5 -1
  22. package/dist/cli/init.js +7 -1
  23. package/dist/editor/Editor.js +39 -16
  24. package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
  25. package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
  26. package/dist/editor/admin/SystemHealthPanel.js +4 -3
  27. package/dist/editor/base-update-wait.d.ts +4 -0
  28. package/dist/editor/base-update-wait.js +7 -0
  29. package/dist/editor/components/ConflictsGroup.d.ts +13 -1
  30. package/dist/editor/components/ConflictsGroup.js +52 -12
  31. package/dist/editor/components/EditorHeader.d.ts +3 -1
  32. package/dist/editor/components/EditorHeader.js +11 -7
  33. package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
  34. package/dist/editor/components/EntryChangesDrawer.js +58 -6
  35. package/dist/editor/conflict-cards.d.ts +4 -1
  36. package/dist/editor/conflict-cards.js +14 -9
  37. package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
  38. package/dist/editor/hooks/useBaseUpdate.js +12 -2
  39. package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
  40. package/dist/editor/hooks/useBranchChangesData.js +13 -0
  41. package/dist/editor/hooks/useBranchManager.d.ts +2 -0
  42. package/dist/editor/hooks/useBranchManager.js +80 -6
  43. package/dist/editor/hooks/useDraftManager.d.ts +8 -1
  44. package/dist/editor/hooks/useDraftManager.js +16 -1
  45. package/dist/editor/hooks/useEntryManager.d.ts +8 -3
  46. package/dist/editor/hooks/useEntryManager.js +6 -5
  47. package/dist/git-manager.d.ts +1 -0
  48. package/dist/git-manager.js +8 -3
  49. package/dist/merge/entry-merge.js +4 -3
  50. package/dist/merge/list-alignment.d.ts +9 -6
  51. package/dist/merge/list-alignment.js +65 -16
  52. package/dist/merge/rekey.d.ts +11 -0
  53. package/dist/merge/rekey.js +75 -0
  54. package/dist/operating-mode/client-safe-strategy.js +1 -1
  55. package/dist/operating-mode/client-unsafe-strategy.js +6 -0
  56. package/dist/operating-mode/types.d.ts +10 -2
  57. package/dist/services.d.ts +12 -4
  58. package/dist/services.js +56 -0
  59. package/dist/types.d.ts +35 -3
  60. package/dist/utils/git.d.ts +7 -0
  61. package/dist/utils/git.js +35 -0
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/worker/branch-merge.d.ts +27 -3
  65. package/dist/worker/branch-merge.js +106 -5
  66. package/dist/worker/branch-policy.d.ts +18 -2
  67. package/dist/worker/branch-policy.js +36 -0
  68. package/dist/worker/cms-worker.d.ts +21 -0
  69. package/dist/worker/cms-worker.js +29 -4
  70. package/dist/worker/conflicts-status.d.ts +20 -0
  71. package/dist/worker/conflicts-status.js +204 -0
  72. package/dist/worker/field-merge.d.ts +17 -2
  73. package/dist/worker/field-merge.js +95 -2
  74. package/dist/worker/git-sync.d.ts +3 -2
  75. package/dist/worker/git-sync.js +9 -0
  76. package/dist/worker/github-commits.d.ts +3 -3
  77. package/dist/worker/github-commits.js +192 -40
  78. package/dist/worker/github-errors.d.ts +15 -0
  79. package/dist/worker/github-errors.js +47 -0
  80. package/dist/worker/github-gateway-http.js +7 -0
  81. package/dist/worker/github-gateway-wire.d.ts +30 -10
  82. package/dist/worker/github-gateway-wire.js +14 -2
  83. package/dist/worker/github-gateway.d.ts +17 -0
  84. package/dist/worker/github-gateway.js +37 -1
  85. package/dist/worker/history-rewrite.d.ts +40 -8
  86. package/dist/worker/history-rewrite.js +51 -23
  87. package/dist/worker/publish-decision.d.ts +37 -0
  88. package/dist/worker/publish-decision.js +16 -0
  89. package/dist/worker/rebase.d.ts +25 -5
  90. package/dist/worker/rebase.js +225 -81
  91. package/dist/worker/task-runner.js +16 -39
  92. package/dist/worker/worker-context.d.ts +6 -0
  93. package/package.json +1 -1
@@ -6,6 +6,10 @@
6
6
  */
7
7
  import path from 'node:path';
8
8
  import { z } from 'zod';
9
+ import { SYNC_BRANCH_ACTION } from '../base-update.js';
10
+ import { operatingStrategy } from '../operating-mode/index.js';
11
+ import { enqueueTask } from '../task-queue/cms-task-queue.js';
12
+ import { getTaskQueueDir } from '../task-queue/task-queue-config.js';
9
13
  import { entryRefFromRepoPath, entryRefFromRepoPathShape, parseEntryParts, } from '../content-listing.js';
10
14
  import { BranchSyncingError, ContentConflictError, ContentStore, ContentStoreError, EntryGoneError, } from '../content-store.js';
11
15
  import { keyOfDataPath } from '../merge/conflict-path.js';
@@ -127,12 +131,28 @@ const mayAccess = (record, checkAccess, user, level) => {
127
131
  return admin;
128
132
  return [record.address, record.recordedAddress ?? record.address].every((at) => checkAccess(at, level).allowed);
129
133
  };
134
+ /**
135
+ * Wake the worker's git sync for `branch`, whose last open choice was just resolved, so the pull
136
+ * request's conflicts status (worker/conflicts-status.ts) turns to success within a sync's rest: a
137
+ * required check would otherwise hold the merge until the next regular cycle. Only where a worker
138
+ * answers the queue; best-effort, as that next cycle sets it anyway.
139
+ */
140
+ async function wakeConflictsStatus(config, branch) {
141
+ if (operatingStrategy(config.mode).baseUpdateRunsInRequest())
142
+ return;
143
+ try {
144
+ await enqueueTask(getTaskQueueDir(config), { action: SYNC_BRANCH_ACTION, payload: { branch } });
145
+ }
146
+ catch (err) {
147
+ canopyLogWarn(`CanopyCMS: Could not queue a sync for ${branch} after its last conflict was resolved:`, getErrorMessage(err));
148
+ }
149
+ }
130
150
  /**
131
151
  * Change the record's entries by `change`, under the record's locks, then `branch.json`
132
- * `conflictFiles` to match. `change` returning null writes nothing. Resolves the entries written,
133
- * or null.
152
+ * `conflictFiles` to match, and wake the worker when that resolved the last open choice.
153
+ * `change` returning null writes nothing. Resolves the entries written, or null.
134
154
  */
135
- const changeRecord = async (branchContext, change) => {
155
+ const changeRecord = async (config, branchContext, change) => {
136
156
  const state = await new MergeConflictStore(branchContext.branchRoot).update((current) => {
137
157
  if (current === null)
138
158
  return null;
@@ -144,6 +164,10 @@ const changeRecord = async (branchContext, change) => {
144
164
  await getBranchMetadataFileManager(branchContext.branchRoot, branchContext.baseRoot).update((existing) => existing?.branch.name === branchContext.branch.name
145
165
  ? { branch: conflictFilesOf(state.entries) }
146
166
  : null, { keepUpdatedAt: true });
167
+ // Each change takes a choice off, so none open now means this one took the last.
168
+ if (openConflictCount(state.entries) === 0 && !state.held) {
169
+ await wakeConflictsStatus(config, branchContext.branch.name);
170
+ }
147
171
  return state.entries;
148
172
  };
149
173
  /**
@@ -152,9 +176,9 @@ const changeRecord = async (branchContext, change) => {
152
176
  * call once the entry is written, inside its locks: the record's own locks come after them in
153
177
  * the one lock order (docs/concurrency.md).
154
178
  */
155
- export async function clearFieldConflicts(branchContext, contentId, resolved) {
179
+ export async function clearFieldConflicts(config, branchContext, contentId, resolved) {
156
180
  let cleared = [];
157
- await changeRecord(branchContext, (entries) => {
181
+ await changeRecord(config, branchContext, (entries) => {
158
182
  const next = withoutResolvedFields(entries, contentId, resolved);
159
183
  cleared = next.cleared;
160
184
  return next.cleared.length > 0 ? next.entries : null;
@@ -408,6 +432,8 @@ const resolveBodySchema = z.object({
408
432
  * field by field with the latest version (`unreadable`).
409
433
  */
410
434
  choice: z.enum(['keep', 'delete', 'restore', 'use-latest']),
435
+ /** The card promised the entry stays deleted: a `keep` is refused if it is in the tree now. */
436
+ entryGone: z.literal(true).optional(),
411
437
  });
412
438
  /** Which kinds of record each choice resolves. */
413
439
  const CHOICE_KINDS = {
@@ -489,10 +515,16 @@ const resolveHandler = async (gc, ctx, req, params, body) => {
489
515
  if (current?.kind !== entry.kind || current.mergeSha !== body.mergeSha) {
490
516
  throw new ConflictChangedError();
491
517
  }
518
+ // Read afresh: a restore since the drawer opened would make "keep it deleted" untrue.
519
+ if (body.entryGone) {
520
+ store.invalidateIndex();
521
+ if ((await store.idIndex()).findById(body.id) !== null)
522
+ throw new ConflictChangedError();
523
+ }
492
524
  };
493
525
  // Clears the record as it stands under its lock, refusing when a merge changed it since.
494
526
  const clear = async () => {
495
- const written = await changeRecord(branchContext, (now) => {
527
+ const written = await changeRecord(ctx.services.config, branchContext, (now) => {
496
528
  const current = now[body.id];
497
529
  if (current?.kind === 'fields' && current.mergeSha === body.mergeSha) {
498
530
  const { [body.id]: _kept, ...rest } = now;
@@ -628,7 +660,7 @@ const resolveEntryConflict = defineEndpoint({
628
660
  * clashed is lost, and the values stay as the merges left them. Under the content-write lock, as a
629
661
  * merge's record is written.
630
662
  */
631
- const releaseHoldHandler = async (gc) => {
663
+ const releaseHoldHandler = async (gc, ctx) => {
632
664
  const { branchRoot } = gc.branchContext;
633
665
  try {
634
666
  const written = await withContentWriteLock(branchRoot, () => new MergeConflictStore(branchRoot).update((current) => {
@@ -636,6 +668,9 @@ const releaseHoldHandler = async (gc) => {
636
668
  return null;
637
669
  return { ...current, held: undefined };
638
670
  }));
671
+ if (written !== null && openConflictCount(written.entries) === 0) {
672
+ await wakeConflictsStatus(ctx.services.config, gc.branchContext.branch.name);
673
+ }
639
674
  return { ok: true, status: 200, data: { released: written !== null } };
640
675
  }
641
676
  catch (err) {
@@ -70,6 +70,7 @@ export interface ApiResponse<TData = unknown> {
70
70
  * another name can pass; no other create failure carries it. `OPEN_CONFLICTS`: a submit or approval
71
71
  * was refused while a merge's conflicts are open on the branch (a 409, merge-conflicts.ts).
72
72
  * `CONFLICT_CHANGED`: a resolve-entry named a conflict the record no longer holds as shown (a 409);
73
- * re-read the branch's conflicts.
73
+ * re-read the branch's conflicts. `BRANCH_MOVED`: an approval or a request for changes named a commit
74
+ * other than the branch's head, or none (a 409, api/branch-review.ts); reload the branch first.
74
75
  */
75
- export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED' | 'OPEN_CONFLICTS' | 'CONFLICT_CHANGED';
76
+ export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED' | 'OPEN_CONFLICTS' | 'CONFLICT_CHANGED' | 'BRANCH_MOVED';
@@ -22,5 +22,11 @@ export declare const BASE_UPDATE_EARLY_RETRY_MS: number;
22
22
  * so a later one is corrupt and is never waited on.
23
23
  */
24
24
  export declare const BASE_UPDATE_CLOCK_SKEW_MS = 60000;
25
+ /**
26
+ * How a run in the request (operating-mode `baseUpdateRunsInRequest`) went: it answered the
27
+ * request; a lock held it back; or the branch clones from a remote other than the workspace's
28
+ * `remote.git`, the only one such a run fetches.
29
+ */
30
+ export type BaseUpdateRun = 'answered' | 'busy' | 'other-remote';
25
31
  /** Whether the branch's latest request for the latest changes is still unanswered. */
26
32
  export declare function isBaseUpdatePending(branch: Pick<BranchMetadata, 'baseUpdateRequestedAt' | 'lastBaseUpdate'>): boolean;
@@ -83,6 +83,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
83
83
  lastAt: string;
84
84
  }>>;
85
85
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
86
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
86
87
  lastGitHubTip: z.ZodOptional<z.ZodString>;
87
88
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
88
89
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -121,6 +122,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
121
122
  name?: string | undefined;
122
123
  }>;
123
124
  at: z.ZodString;
125
+ commit: z.ZodOptional<z.ZodString>;
124
126
  }, "strip", z.ZodTypeAny, {
125
127
  at: string;
126
128
  state: "approved" | "changes-requested";
@@ -128,6 +130,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
128
130
  userId: string;
129
131
  name?: string | undefined;
130
132
  };
133
+ commit?: string | undefined;
131
134
  }, {
132
135
  at: string;
133
136
  state: "approved" | "changes-requested";
@@ -135,8 +138,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
135
138
  userId: string;
136
139
  name?: string | undefined;
137
140
  };
141
+ commit?: string | undefined;
138
142
  }>>>;
139
143
  baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
144
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
140
145
  lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
141
146
  at: z.ZodString;
142
147
  outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
@@ -216,6 +221,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
216
221
  lastAt: string;
217
222
  }>>;
218
223
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
224
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
219
225
  lastGitHubTip: z.ZodOptional<z.ZodString>;
220
226
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
221
227
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -254,6 +260,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
254
260
  name?: string | undefined;
255
261
  }>;
256
262
  at: z.ZodString;
263
+ commit: z.ZodOptional<z.ZodString>;
257
264
  }, "strip", z.ZodTypeAny, {
258
265
  at: string;
259
266
  state: "approved" | "changes-requested";
@@ -261,6 +268,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
261
268
  userId: string;
262
269
  name?: string | undefined;
263
270
  };
271
+ commit?: string | undefined;
264
272
  }, {
265
273
  at: string;
266
274
  state: "approved" | "changes-requested";
@@ -268,8 +276,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
268
276
  userId: string;
269
277
  name?: string | undefined;
270
278
  };
279
+ commit?: string | undefined;
271
280
  }>>>;
272
281
  baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
282
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
273
283
  lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
274
284
  at: z.ZodString;
275
285
  outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
@@ -349,6 +359,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
349
359
  lastAt: string;
350
360
  }>>;
351
361
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
362
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
352
363
  lastGitHubTip: z.ZodOptional<z.ZodString>;
353
364
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
354
365
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -387,6 +398,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
387
398
  name?: string | undefined;
388
399
  }>;
389
400
  at: z.ZodString;
401
+ commit: z.ZodOptional<z.ZodString>;
390
402
  }, "strip", z.ZodTypeAny, {
391
403
  at: string;
392
404
  state: "approved" | "changes-requested";
@@ -394,6 +406,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
394
406
  userId: string;
395
407
  name?: string | undefined;
396
408
  };
409
+ commit?: string | undefined;
397
410
  }, {
398
411
  at: string;
399
412
  state: "approved" | "changes-requested";
@@ -401,8 +414,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
401
414
  userId: string;
402
415
  name?: string | undefined;
403
416
  };
417
+ commit?: string | undefined;
404
418
  }>>>;
405
419
  baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
420
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
406
421
  lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
407
422
  at: z.ZodString;
408
423
  outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
@@ -82,6 +82,7 @@ const branchMetadataShape = {
82
82
  .object({ message: z.string(), firstAt: z.string(), lastAt: z.string() })
83
83
  .optional(),
84
84
  historyRewrittenFrom: z.string().optional(),
85
+ historyRewrittenTo: z.string().optional(),
85
86
  lastGitHubTip: z.string().optional(),
86
87
  keptOverGitHub: z.array(z.string()).optional(),
87
88
  rewrittenOnGitHub: z.string().optional(),
@@ -99,16 +100,19 @@ const branchMetadataShape = {
99
100
  ])
100
101
  .optional()
101
102
  .catch(undefined),
102
- // Dropped when malformed: it only decorates the PR body, so it never fails a read.
103
+ // Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
104
+ // the worker judges a move against the clone's head before it (worker/github-commits.ts).
103
105
  review: z
104
106
  .object({
105
107
  state: z.enum(['approved', 'changes-requested']),
106
108
  by: z.object({ userId: z.string(), name: z.string().optional() }),
107
109
  at: z.string(),
110
+ commit: z.string().optional(),
108
111
  })
109
112
  .optional()
110
113
  .catch(undefined),
111
114
  baseUpdateRequestedAt: z.string().optional().catch(undefined),
115
+ latestChangesWaiting: z.string().optional().catch(undefined),
112
116
  // Dropped when malformed: it only ends the editor's wait, which then times out instead.
113
117
  lastBaseUpdate: z
114
118
  .object({