canopycms-cdk 0.0.59 → 0.0.60

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.
@@ -4380,7 +4380,7 @@ var init_media = __esm({
4380
4380
  });
4381
4381
 
4382
4382
  // ../canopycms/src/config/schemas/config.ts
4383
- var defaultBranchAccessSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema;
4383
+ var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema;
4384
4384
  var init_config = __esm({
4385
4385
  "../canopycms/src/config/schemas/config.ts"() {
4386
4386
  "use strict";
@@ -4388,7 +4388,13 @@ var init_config = __esm({
4388
4388
  init_collection();
4389
4389
  init_media();
4390
4390
  defaultBranchAccessSchema = external_exports.enum(["allow", "deny"]).default("deny");
4391
- defaultPathAccessSchema = external_exports.enum(["allow", "deny"]).default("deny");
4391
+ defaultPathAccessLevelSchema = external_exports.enum(["allow", "deny"]);
4392
+ defaultPathAccessLevelsSchema = external_exports.object({
4393
+ read: defaultPathAccessLevelSchema.optional(),
4394
+ edit: defaultPathAccessLevelSchema.optional(),
4395
+ review: defaultPathAccessLevelSchema.optional()
4396
+ }).strict();
4397
+ defaultPathAccessSchema = external_exports.union([defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema]).default("deny");
4392
4398
  defaultBaseBranchSchema = external_exports.string().default("main");
4393
4399
  defaultRemoteNameSchema = external_exports.string().default("origin");
4394
4400
  defaultRemoteUrlSchema = external_exports.string().min(1);
@@ -4421,6 +4427,10 @@ var init_config = __esm({
4421
4427
  // have .default('deny'), so the field is optional on input but always resolves to
4422
4428
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
4423
4429
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
4430
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
4431
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
4432
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
4433
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
4424
4434
  defaultBranchAccess: defaultBranchAccessSchema,
4425
4435
  defaultPathAccess: defaultPathAccessSchema,
4426
4436
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -6727,7 +6737,7 @@ var require_node = __commonJS({
6727
6737
  var tty = require("tty");
6728
6738
  var util2 = require("util");
6729
6739
  exports2.init = init;
6730
- exports2.log = log9;
6740
+ exports2.log = log10;
6731
6741
  exports2.formatArgs = formatArgs;
6732
6742
  exports2.save = save;
6733
6743
  exports2.load = load;
@@ -6862,7 +6872,7 @@ var require_node = __commonJS({
6862
6872
  }
6863
6873
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
6864
6874
  }
6865
- function log9(...args) {
6875
+ function log10(...args) {
6866
6876
  return process.stderr.write(util2.formatWithOptions(exports2.inspectOpts, ...args) + "\n");
6867
6877
  }
6868
6878
  function save(namespaces) {
@@ -6916,27 +6926,27 @@ var require_src2 = __commonJS({
6916
6926
  Object.defineProperty(exports2, "__esModule", { value: true });
6917
6927
  var fs_1 = require("fs");
6918
6928
  var debug_1 = __importDefault(require_src());
6919
- var log9 = debug_1.default("@kwsites/file-exists");
6929
+ var log10 = debug_1.default("@kwsites/file-exists");
6920
6930
  function check(path3, isFile, isDirectory) {
6921
- log9(`checking %s`, path3);
6931
+ log10(`checking %s`, path3);
6922
6932
  try {
6923
6933
  const stat = fs_1.statSync(path3);
6924
6934
  if (stat.isFile() && isFile) {
6925
- log9(`[OK] path represents a file`);
6935
+ log10(`[OK] path represents a file`);
6926
6936
  return true;
6927
6937
  }
6928
6938
  if (stat.isDirectory() && isDirectory) {
6929
- log9(`[OK] path represents a directory`);
6939
+ log10(`[OK] path represents a directory`);
6930
6940
  return true;
6931
6941
  }
6932
- log9(`[FAIL] path represents something other than a file or directory`);
6942
+ log10(`[FAIL] path represents something other than a file or directory`);
6933
6943
  return false;
6934
6944
  } catch (e) {
6935
6945
  if (e.code === "ENOENT") {
6936
- log9(`[FAIL] path is not accessible: %o`, e);
6946
+ log10(`[FAIL] path is not accessible: %o`, e);
6937
6947
  return false;
6938
6948
  }
6939
- log9(`[FATAL] %o`, e);
6949
+ log10(`[FATAL] %o`, e);
6940
6950
  throw e;
6941
6951
  }
6942
6952
  }
@@ -12528,7 +12538,7 @@ var require_merge2 = __commonJS({
12528
12538
  var require_addPairToJSMap = __commonJS({
12529
12539
  "../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
12530
12540
  "use strict";
12531
- var log9 = require_log();
12541
+ var log10 = require_log();
12532
12542
  var merge = require_merge2();
12533
12543
  var stringify = require_stringify2();
12534
12544
  var identity = require_identity();
@@ -12577,7 +12587,7 @@ var require_addPairToJSMap = __commonJS({
12577
12587
  let jsonStr = JSON.stringify(strKey);
12578
12588
  if (jsonStr.length > 40)
12579
12589
  jsonStr = jsonStr.substring(0, 36) + '..."';
12580
- log9.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
12590
+ log10.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
12581
12591
  ctx.mapKeyWarned = true;
12582
12592
  }
12583
12593
  return strKey;
@@ -17973,7 +17983,7 @@ var require_public_api = __commonJS({
17973
17983
  var composer = require_composer();
17974
17984
  var Document = require_Document();
17975
17985
  var errors = require_errors();
17976
- var log9 = require_log();
17986
+ var log10 = require_log();
17977
17987
  var identity = require_identity();
17978
17988
  var lineCounter = require_line_counter();
17979
17989
  var parser4 = require_parser();
@@ -18025,7 +18035,7 @@ var require_public_api = __commonJS({
18025
18035
  const doc = parseDocument(src, options2);
18026
18036
  if (!doc)
18027
18037
  return null;
18028
- doc.warnings.forEach((warning) => log9.warn(doc.options.logLevel, warning));
18038
+ doc.warnings.forEach((warning) => log10.warn(doc.options.logLevel, warning));
18029
18039
  if (doc.errors.length > 0) {
18030
18040
  if (doc.options.logLevel !== "silent")
18031
18041
  throw doc.errors[0];
@@ -19062,7 +19072,7 @@ var require_dist_node5 = __commonJS({
19062
19072
  }
19063
19073
  function fetchWrapper(requestOptions) {
19064
19074
  var _a3, _b, _c, _d;
19065
- const log9 = requestOptions.request && requestOptions.request.log ? requestOptions.request.log : console;
19075
+ const log10 = requestOptions.request && requestOptions.request.log ? requestOptions.request.log : console;
19066
19076
  const parseSuccessResponseBody = ((_a3 = requestOptions.request) == null ? void 0 : _a3.parseSuccessResponseBody) !== false;
19067
19077
  if (isPlainObject(requestOptions.body) || Array.isArray(requestOptions.body)) {
19068
19078
  requestOptions.body = JSON.stringify(requestOptions.body);
@@ -19097,7 +19107,7 @@ var require_dist_node5 = __commonJS({
19097
19107
  if ("deprecation" in headers) {
19098
19108
  const matches = headers.link && headers.link.match(/<([^<>]+)>; rel="deprecation"/);
19099
19109
  const deprecationLink = matches && matches.pop();
19100
- log9.warn(
19110
+ log10.warn(
19101
19111
  `[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
19102
19112
  );
19103
19113
  }
@@ -25585,11 +25595,11 @@ function taskCallback(task, response, callback = NOOP) {
25585
25595
  response.then(onSuccess, onError2);
25586
25596
  }
25587
25597
  function addDeprecationNoticeToError(err2) {
25588
- let log9 = (name) => {
25598
+ let log10 = (name) => {
25589
25599
  console.warn(
25590
25600
  `simple-git deprecation notice: accessing GitResponseError.${name} should be GitResponseError.git.${name}, this will no longer be available in version 3`
25591
25601
  );
25592
- log9 = NOOP;
25602
+ log10 = NOOP;
25593
25603
  };
25594
25604
  return Object.create(err2, Object.getOwnPropertyNames(err2.git).reduce(descriptorReducer, {}));
25595
25605
  function descriptorReducer(all, name) {
@@ -25600,7 +25610,7 @@ function addDeprecationNoticeToError(err2) {
25600
25610
  enumerable: false,
25601
25611
  configurable: false,
25602
25612
  get() {
25603
- log9(name);
25613
+ log10(name);
25604
25614
  return err2.git[name];
25605
25615
  }
25606
25616
  };
@@ -29882,9 +29892,12 @@ var permissionPathSchema = external_exports.string().min(1).transform((val, ctx)
29882
29892
  return result.path;
29883
29893
  });
29884
29894
  var PermissionsFileSchema = external_exports.object({
29885
- version: external_exports.literal(1),
29886
- contentVersion: external_exports.number().optional(),
29887
- // For optimistic locking
29895
+ // Managed by writeOccJsonFile (see authorization/settings-file-store.ts) —
29896
+ // the single OCC counter for this file. Optional so a hand-written file
29897
+ // with no version field parses as version 0. A legacy `contentVersion`
29898
+ // field (if present) is silently stripped by this non-strict zod parse.
29899
+ version: external_exports.number().int().nonnegative().optional(),
29900
+ writeId: external_exports.string().optional(),
29888
29901
  updatedAt: external_exports.string().datetime(),
29889
29902
  updatedBy: external_exports.string(),
29890
29903
  pathPermissions: external_exports.array(
@@ -29900,12 +29913,18 @@ var PermissionsFileSchema = external_exports.object({
29900
29913
  // ../canopycms/src/authorization/permissions/loader.ts
29901
29914
  init_operating_mode();
29902
29915
 
29916
+ // ../canopycms/src/authorization/settings-file-store.ts
29917
+ init_error();
29918
+
29903
29919
  // ../canopycms/src/authorization/groups/schema.ts
29904
29920
  init_zod();
29905
29921
  var GroupsFileSchema = external_exports.object({
29906
- version: external_exports.literal(1),
29907
- contentVersion: external_exports.number().optional(),
29908
- // For optimistic locking
29922
+ // Managed by writeOccJsonFile (see authorization/settings-file-store.ts) —
29923
+ // the single OCC counter for this file. Optional so a hand-written file
29924
+ // with no version field parses as version 0. A legacy `contentVersion`
29925
+ // field (if present) is silently stripped by this non-strict zod parse.
29926
+ version: external_exports.number().int().nonnegative().optional(),
29927
+ writeId: external_exports.string().optional(),
29909
29928
  updatedAt: external_exports.string().datetime(),
29910
29929
  updatedBy: external_exports.string(),
29911
29930
  groups: external_exports.array(
@@ -29952,6 +29971,9 @@ init_error();
29952
29971
  init_id();
29953
29972
  var log8 = createDebugLogger({ prefix: "ContentListing" });
29954
29973
 
29974
+ // ../canopycms/src/context.ts
29975
+ var log9 = createDebugLogger({ prefix: "Context" });
29976
+
29955
29977
  // ../canopycms/src/server.ts
29956
29978
  init_operating_mode();
29957
29979
  init_schema();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopycms-cdk",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "description": "AWS CDK constructs and EC2 worker for CanopyCMS deployment",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,8 +53,8 @@
53
53
  "tsx": "^4.19.0",
54
54
  "typescript": "^5.6.3",
55
55
  "vitest": "^4.1.0",
56
- "canopycms": "0.0.59",
57
- "canopycms-auth-clerk": "0.0.59"
56
+ "canopycms-auth-clerk": "0.0.60",
57
+ "canopycms": "0.0.60"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsc",
@@ -41488,7 +41488,13 @@ var mediaSchema = external_exports.discriminatedUnion("adapter", [
41488
41488
 
41489
41489
  // ../canopycms/src/config/schemas/config.ts
41490
41490
  var defaultBranchAccessSchema = external_exports.enum(["allow", "deny"]).default("deny");
41491
- var defaultPathAccessSchema = external_exports.enum(["allow", "deny"]).default("deny");
41491
+ var defaultPathAccessLevelSchema = external_exports.enum(["allow", "deny"]);
41492
+ var defaultPathAccessLevelsSchema = external_exports.object({
41493
+ read: defaultPathAccessLevelSchema.optional(),
41494
+ edit: defaultPathAccessLevelSchema.optional(),
41495
+ review: defaultPathAccessLevelSchema.optional()
41496
+ }).strict();
41497
+ var defaultPathAccessSchema = external_exports.union([defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema]).default("deny");
41492
41498
  var defaultBaseBranchSchema = external_exports.string().default("main");
41493
41499
  var defaultRemoteNameSchema = external_exports.string().default("origin");
41494
41500
  var defaultRemoteUrlSchema = external_exports.string().min(1);
@@ -41521,6 +41527,10 @@ var CanopyConfigSchema = external_exports.object({
41521
41527
  // have .default('deny'), so the field is optional on input but always resolves to
41522
41528
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
41523
41529
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
41530
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
41531
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
41532
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
41533
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
41524
41534
  defaultBranchAccess: defaultBranchAccessSchema,
41525
41535
  defaultPathAccess: defaultPathAccessSchema,
41526
41536
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -41898,6 +41908,14 @@ function isValidContentId(id) {
41898
41908
  return CONTENT_ID_PATTERN.test(id);
41899
41909
  }
41900
41910
 
41911
+ // ../canopycms/src/paths/branch.ts
41912
+ function sanitizeBranchName(branchName) {
41913
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
41914
+ const squashed = replaced.replace(/-+/g, "-");
41915
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
41916
+ return trimmedDots || "branch";
41917
+ }
41918
+
41901
41919
  // ../canopycms/src/utils/async-mutex.ts
41902
41920
  var locks = /* @__PURE__ */ new Map();
41903
41921
  async function withLock(key, fn) {
@@ -42184,7 +42202,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
42184
42202
  createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
42185
42203
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
42186
42204
  // Fork point is recorded once at creation; later saves must not move it
42187
- baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch
42205
+ baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
42206
+ // Always stamped fresh — the spreads above would otherwise let
42207
+ // the existing (creation-time) value win forever, freezing the
42208
+ // timestamp the editor's Branches panel sorts and displays by
42209
+ updatedAt: now
42188
42210
  }
42189
42211
  };
42190
42212
  const written = await this.write(merged, version);
@@ -42211,6 +42233,14 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
42211
42233
  await registry2.invalidate();
42212
42234
  }
42213
42235
  };
42236
+ function buildMergedBranchUpdate(branchName, now = /* @__PURE__ */ new Date()) {
42237
+ return {
42238
+ name: branchName,
42239
+ status: "archived",
42240
+ pullRequestState: "merged",
42241
+ mergedAt: now.toISOString()
42242
+ };
42243
+ }
42214
42244
  var getBranchMetadataFileManager = (branchRoot, baseRoot, options) => {
42215
42245
  return BranchMetadataFileManager.get(branchRoot, baseRoot, options);
42216
42246
  };
@@ -42380,6 +42410,7 @@ var CmsWorker = class {
42380
42410
  this.remoteGitPath = path9.join(config.workspacePath, "remote.git");
42381
42411
  this.contentBranchesPath = path9.join(config.workspacePath, "content-branches");
42382
42412
  this.baseBranch = config.baseBranch ?? "main";
42413
+ this.sanitizedBaseBranch = sanitizeBranchName(this.baseBranch);
42383
42414
  this.maxTasksPerCycle = config.maxTasksPerCycle ?? 10;
42384
42415
  this.taskTimeoutMs = config.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT;
42385
42416
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -42773,7 +42804,14 @@ var CmsWorker = class {
42773
42804
  syncStatus: "synced"
42774
42805
  };
42775
42806
  if (result.prUrl) updates.pullRequestUrl = result.prUrl;
42776
- if (result.prNumber) updates.pullRequestNumber = result.prNumber;
42807
+ if (result.prNumber) {
42808
+ updates.pullRequestNumber = result.prNumber;
42809
+ const current = await BranchMetadataFileManager.loadOnly(branchPath);
42810
+ const terminal = current?.branch.status === "archived" || current?.branch.pullRequestState === "merged" || current?.branch.pullRequestState === "closed";
42811
+ if (!terminal) {
42812
+ updates.pullRequestState = "open";
42813
+ }
42814
+ }
42777
42815
  await meta.save({ branch: updates });
42778
42816
  } catch (err) {
42779
42817
  console.error(
@@ -42855,9 +42893,154 @@ var CmsWorker = class {
42855
42893
  await git.raw(["fetch", this.buildGitHubUrl(), "--prune", "+refs/heads/*:refs/heads/*"]);
42856
42894
  console.log("Fetched from GitHub");
42857
42895
  await this.pushSettingsBranches(git);
42896
+ await this.refreshBaseBranchWorkspace();
42858
42897
  await this.rebaseActiveBranches();
42859
42898
  await cleanupOldTasks(this.taskDir, void 0, this.log);
42860
42899
  }
42900
+ /**
42901
+ * Fast-forward the base branch's own working-tree clone
42902
+ * (content-branches/<baseBranch>) to match origin/<baseBranch>.
42903
+ *
42904
+ * Previously this clone was refreshed only incidentally, by the generic
42905
+ * rebase loop below (rebaseActiveBranches): for a branch with status
42906
+ * 'editing', rebasing onto origin/<baseBranch> degenerates to a
42907
+ * fast-forward when the clone IS the base branch. But that loop's skip
42908
+ * paths -- a dirty tree, a missing .git -- are silent, which is the
42909
+ * suspected live failure mode: a wedged base clone with no diagnosable
42910
+ * signal in the logs. This dedicated step makes the refresh explicit,
42911
+ * ff-only, and loud, so a stuck base view (an editor forking a new branch
42912
+ * "from base" that's actually a stale snapshot) is diagnosable from logs.
42913
+ * This runs every sync cycle so the drift window is bounded by
42914
+ * gitSyncInterval.
42915
+ *
42916
+ * ff-only on purpose: this clone must stay a linear mirror of
42917
+ * origin/<baseBranch>, so a merge that isn't a fast-forward (diverged
42918
+ * local history) is treated as a should-never-happen condition and left
42919
+ * untouched rather than force-resolved.
42920
+ */
42921
+ async refreshBaseBranchWorkspace() {
42922
+ const basePath32 = path9.join(this.contentBranchesPath, this.sanitizedBaseBranch);
42923
+ const gitDir = path9.join(basePath32, ".git");
42924
+ try {
42925
+ let gitDirStat;
42926
+ try {
42927
+ gitDirStat = await fs7.stat(gitDir);
42928
+ } catch {
42929
+ gitDirStat = null;
42930
+ }
42931
+ if (!gitDirStat || !gitDirStat.isDirectory()) {
42932
+ console.log(
42933
+ `Base branch workspace (${this.baseBranch}): not yet provisioned, skipping refresh`
42934
+ );
42935
+ return;
42936
+ }
42937
+ const baseGit = simpleGit({
42938
+ baseDir: basePath32,
42939
+ // Keep git non-interactive during the merge so it never blocks on an
42940
+ // editor. simple-git >=3.32 blocks setting core.editor unless
42941
+ // explicitly opted in; the value here is a hardcoded literal
42942
+ // ("true", the shell no-op), not user input, so enabling
42943
+ // allowUnsafeEditor carries no injection risk (same as the rebase
42944
+ // loop's git config below).
42945
+ config: ["core.editor=true"],
42946
+ unsafe: { allowUnsafeEditor: true },
42947
+ // DEP-H1: a hung fetch/merge against this EFS-backed clone would
42948
+ // stall the sync loop forever (scheduleLoop only reschedules after
42949
+ // completion). The block timeout is inactivity-based, so a
42950
+ // slow-but-flowing transfer is unaffected -- same as syncGit()'s
42951
+ // remote handle.
42952
+ timeout: { block: this.taskTimeoutMs }
42953
+ });
42954
+ const status = await baseGit.status();
42955
+ const trackedDirty = status.files.filter((f5) => f5.index !== "?" || f5.working_dir !== "?");
42956
+ if (trackedDirty.length > 0) {
42957
+ console.error(
42958
+ `Base branch workspace (${this.baseBranch}) has uncommitted changes -- skipping refresh. Dirty files: ${trackedDirty.map((f5) => f5.path).join(", ")}`
42959
+ );
42960
+ return;
42961
+ }
42962
+ await baseGit.fetch("origin", this.baseBranch);
42963
+ const behindCount = parseInt(
42964
+ (await baseGit.raw(["rev-list", "--count", `HEAD..origin/${this.baseBranch}`])).trim(),
42965
+ 10
42966
+ );
42967
+ if (behindCount > 0) {
42968
+ try {
42969
+ await baseGit.merge(["--ff-only", `origin/${this.baseBranch}`]);
42970
+ } catch (err) {
42971
+ console.error(
42972
+ `Base branch workspace (${this.baseBranch}) failed to fast-forward (diverged local history?): ${getErrorMessage2(err)}`
42973
+ );
42974
+ return;
42975
+ }
42976
+ await invalidateBranchContentCaches(basePath32);
42977
+ }
42978
+ const currentMeta = await BranchMetadataFileManager.loadOnly(basePath32);
42979
+ const conflictStatus = currentMeta?.branch.conflictStatus;
42980
+ const conflictFiles = currentMeta?.branch.conflictFiles;
42981
+ const alreadyClean = (conflictStatus === void 0 || conflictStatus === "clean") && (conflictFiles === void 0 || conflictFiles.length === 0);
42982
+ if (!alreadyClean) {
42983
+ const meta = getBranchMetadataFileManager(basePath32, this.contentBranchesPath);
42984
+ await meta.save({
42985
+ branch: { name: this.baseBranch, conflictStatus: "clean", conflictFiles: [] }
42986
+ });
42987
+ }
42988
+ console.log(
42989
+ behindCount > 0 ? `Base branch workspace (${this.baseBranch}): fast-forwarded ${behindCount} commit(s)` : `Base branch workspace (${this.baseBranch}): up to date`
42990
+ );
42991
+ } catch (err) {
42992
+ console.error(
42993
+ `Base branch workspace (${this.baseBranch}) refresh failed: ${getErrorMessage2(err)}`
42994
+ );
42995
+ }
42996
+ }
42997
+ /**
42998
+ * Poll GitHub for a submitted/approved branch's PR resolution.
42999
+ *
43000
+ * submitted/approved branches sit outside the rebase loop and get no
43001
+ * other signal that their PR resolved on GitHub -- nothing pushes a
43002
+ * merge/close webhook back into the branch workspace. merged ->
43003
+ * auto-archive via buildMergedBranchUpdate (shared with the manual
43004
+ * markAsMerged API so both paths produce identical archived-branch
43005
+ * metadata). closed-without-merge -> record pullRequestState only; an
43006
+ * admin decides the workflow transition from there. Best-effort: any
43007
+ * failure here is logged and swallowed, retried next sync cycle.
43008
+ */
43009
+ async pollMergeState(branchDir, branchPath, metaFile) {
43010
+ const prNumber = metaFile?.branch.pullRequestNumber;
43011
+ if (!prNumber) return;
43012
+ const controller = new AbortController();
43013
+ const timer = setTimeout(() => controller.abort(), this.taskTimeoutMs);
43014
+ try {
43015
+ const { data: data2 } = await this.octokit.pulls.get({
43016
+ owner: this.config.githubOwner,
43017
+ repo: this.config.githubRepo,
43018
+ pull_number: prNumber,
43019
+ request: { signal: controller.signal }
43020
+ });
43021
+ if (data2.merged) {
43022
+ const meta2 = getBranchMetadataFileManager(branchPath, this.contentBranchesPath);
43023
+ await meta2.save({
43024
+ branch: buildMergedBranchUpdate(
43025
+ branchDir,
43026
+ data2.merged_at ? new Date(data2.merged_at) : void 0
43027
+ )
43028
+ });
43029
+ console.log(` PR #${prNumber} for ${branchDir} is merged -> archived`);
43030
+ return;
43031
+ }
43032
+ const newState = data2.state === "closed" ? "closed" : "open";
43033
+ const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
43034
+ if (currentMeta?.branch.pullRequestState === newState) return;
43035
+ const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath);
43036
+ await meta.save({ branch: { name: branchDir, pullRequestState: newState } });
43037
+ console.log(` PR #${prNumber} for ${branchDir}: pullRequestState -> ${newState}`);
43038
+ } catch (err) {
43039
+ console.warn(` Failed to poll PR #${prNumber} for ${branchDir}: ${getErrorMessage2(err)}`);
43040
+ } finally {
43041
+ clearTimeout(timer);
43042
+ }
43043
+ }
42861
43044
  async rebaseActiveBranches() {
42862
43045
  let branchDirs;
42863
43046
  try {
@@ -42866,18 +43049,34 @@ var CmsWorker = class {
42866
43049
  return;
42867
43050
  }
42868
43051
  for (const branchDir of branchDirs) {
43052
+ if (branchDir.startsWith(".") || branchDir === "branches.json") {
43053
+ continue;
43054
+ }
42869
43055
  const branchPath = path9.join(this.contentBranchesPath, branchDir);
42870
43056
  const gitDir = path9.join(branchPath, ".git");
42871
43057
  try {
42872
43058
  const stat = await fs7.stat(gitDir);
42873
- if (!stat.isDirectory()) continue;
43059
+ if (!stat.isDirectory()) {
43060
+ console.log(` Skipping ${branchDir}: .git is not a directory`);
43061
+ continue;
43062
+ }
42874
43063
  } catch {
43064
+ console.log(` Skipping ${branchDir}: no .git directory (not a branch workspace)`);
43065
+ continue;
43066
+ }
43067
+ if (branchDir === this.sanitizedBaseBranch) {
43068
+ console.log(` Skipping ${branchDir}: base branch (refreshed separately)`);
42875
43069
  continue;
42876
43070
  }
42877
43071
  try {
42878
43072
  const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
42879
43073
  const branchStatus2 = metaFile?.branch.status;
42880
- if (branchStatus2 === "submitted" || branchStatus2 === "approved" || branchStatus2 === "archived") {
43074
+ if (branchStatus2 === "submitted" || branchStatus2 === "approved") {
43075
+ console.log(` Skipping ${branchDir} (${branchStatus2})`);
43076
+ await this.pollMergeState(branchDir, branchPath, metaFile);
43077
+ continue;
43078
+ }
43079
+ if (branchStatus2 === "archived") {
42881
43080
  console.log(` Skipping ${branchDir} (${branchStatus2})`);
42882
43081
  continue;
42883
43082
  }