canopycms 0.0.58 → 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.
- package/dist/api/__test__/mock-client.js +4 -4
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/branch-merge.d.ts.map +1 -1
- package/dist/api/branch-merge.js +2 -6
- package/dist/api/branch-merge.js.map +1 -1
- package/dist/api/branch-withdraw.d.ts.map +1 -1
- package/dist/api/branch-withdraw.js +19 -4
- package/dist/api/branch-withdraw.js.map +1 -1
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +22 -3
- package/dist/api/entries.js.map +1 -1
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.d.ts.map +1 -1
- package/dist/api/groups.js +89 -68
- package/dist/api/groups.js.map +1 -1
- package/dist/api/permissions.d.ts +1 -0
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +23 -20
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +29 -15
- package/dist/api/schema.js.map +1 -1
- package/dist/authorization/groups/index.d.ts +1 -1
- package/dist/authorization/groups/index.d.ts.map +1 -1
- package/dist/authorization/groups/index.js +1 -1
- package/dist/authorization/groups/index.js.map +1 -1
- package/dist/authorization/groups/loader.d.ts +25 -4
- package/dist/authorization/groups/loader.d.ts.map +1 -1
- package/dist/authorization/groups/loader.js +39 -20
- package/dist/authorization/groups/loader.js.map +1 -1
- package/dist/authorization/groups/schema.d.ts +8 -7
- package/dist/authorization/groups/schema.d.ts.map +1 -1
- package/dist/authorization/groups/schema.js +8 -4
- package/dist/authorization/groups/schema.js.map +1 -1
- package/dist/authorization/index.d.ts +4 -3
- package/dist/authorization/index.d.ts.map +1 -1
- package/dist/authorization/index.js +6 -3
- package/dist/authorization/index.js.map +1 -1
- package/dist/authorization/path.d.ts +8 -0
- package/dist/authorization/path.d.ts.map +1 -1
- package/dist/authorization/path.js +16 -1
- package/dist/authorization/path.js.map +1 -1
- package/dist/authorization/permissions/index.d.ts +1 -1
- package/dist/authorization/permissions/index.d.ts.map +1 -1
- package/dist/authorization/permissions/index.js +1 -1
- package/dist/authorization/permissions/index.js.map +1 -1
- package/dist/authorization/permissions/loader.d.ts +18 -4
- package/dist/authorization/permissions/loader.d.ts.map +1 -1
- package/dist/authorization/permissions/loader.js +35 -24
- package/dist/authorization/permissions/loader.js.map +1 -1
- package/dist/authorization/permissions/schema.d.ts +8 -7
- package/dist/authorization/permissions/schema.d.ts.map +1 -1
- package/dist/authorization/permissions/schema.js +8 -4
- package/dist/authorization/permissions/schema.js.map +1 -1
- package/dist/authorization/settings-file-store.d.ts +127 -0
- package/dist/authorization/settings-file-store.d.ts.map +1 -0
- package/dist/authorization/settings-file-store.js +156 -0
- package/dist/authorization/settings-file-store.js.map +1 -0
- package/dist/branch-metadata.d.ts +11 -0
- package/dist/branch-metadata.d.ts.map +1 -1
- package/dist/branch-metadata.js +22 -0
- package/dist/branch-metadata.js.map +1 -1
- package/dist/branch-workspace.d.ts.map +1 -1
- package/dist/branch-workspace.js +1 -0
- package/dist/branch-workspace.js.map +1 -1
- package/dist/cli/cli.d.ts +20 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +9618 -246
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/generate-ai-content.js +101 -12
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +25 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +39 -11
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/template-files/Dockerfile.cms.template +33 -1
- package/dist/cli/template-files/deploy-cms.yml.template +9 -1
- package/dist/cli/template-files/dockerignore.template +13 -0
- package/dist/cli/template-files/middleware-clerk.ts.template +12 -5
- package/dist/cli/template-files/middleware.ts.template +10 -5
- package/dist/cli/templates.d.ts +1 -0
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +3 -0
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schemas/config.d.ts +68 -6
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +32 -2
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +31 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -3
- package/dist/context.js.map +1 -1
- package/dist/editor/BranchManager.d.ts +3 -0
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +18 -5
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +3 -1
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +2 -0
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useGroupManager.d.ts.map +1 -1
- package/dist/editor/hooks/useGroupManager.js +13 -2
- package/dist/editor/hooks/useGroupManager.js.map +1 -1
- package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
- package/dist/editor/hooks/usePermissionManager.js +13 -2
- package/dist/editor/hooks/usePermissionManager.js.map +1 -1
- package/dist/git-manager.d.ts +39 -0
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +104 -8
- package/dist/git-manager.js.map +1 -1
- package/dist/schema/schema-store.d.ts +98 -0
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +187 -29
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +1 -0
- package/dist/settings-workspace.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/git.d.ts +19 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +47 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +64 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +318 -28
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +1 -1
package/dist/utils/git.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAItC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAgB,EAChB,WAAmB,MAAM;IAEzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAClE,OAAO,IAAI,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAA;IACjB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAKvC;IACC,IAAI,OAAO,CAAC,iBAAiB;QAAE,OAAO,OAAO,CAAC,iBAAiB,CAAA;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAItC,wEAAwE;AACxE,MAAM,sBAAsB,GAAG,yBAAyB,CAAA;AACxD,6EAA6E;AAC7E,8EAA8E;AAC9E,6DAA6D;AAC7D,MAAM,wBAAwB,GAAG,uBAAuB,CAAA;AACxD,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,sBAAsB,GAAG,IAAI,CAAA;AACnC,uEAAuE;AACvE,6EAA6E;AAC7E,8EAA8E;AAC9E,uEAAuE;AACvE,8BAA8B;AAC9B,MAAM,qBAAqB,GAAG,iBAAiB,CAAA;AAC/C,MAAM,0BAA0B,GAAG,aAAa,CAAA;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACzC,IAAI,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACjD,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACnD,IAAI,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACjD,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACjD,OAAO,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAgB,EAChB,WAAmB,MAAM;IAEzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAClE,OAAO,IAAI,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAA;IACjB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAKvC;IACC,IAAI,OAAO,CAAC,iBAAiB;QAAE,OAAO,OAAO,CAAC,iBAAiB,CAAA;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -92,6 +92,7 @@ export declare class CmsWorker {
|
|
|
92
92
|
private remoteGitPath;
|
|
93
93
|
private contentBranchesPath;
|
|
94
94
|
private baseBranch;
|
|
95
|
+
private sanitizedBaseBranch;
|
|
95
96
|
private activeTimeouts;
|
|
96
97
|
private running;
|
|
97
98
|
private activeOperations;
|
|
@@ -139,9 +140,37 @@ export declare class CmsWorker {
|
|
|
139
140
|
* preventing overlapping executions.
|
|
140
141
|
*/
|
|
141
142
|
private scheduleLoop;
|
|
143
|
+
/**
|
|
144
|
+
* Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
|
|
145
|
+
*
|
|
146
|
+
* Uses an explicit `--git-dir` invocation rather than `simpleGit({ baseDir
|
|
147
|
+
* })` so this also works in sandboxed/CI git environments that set
|
|
148
|
+
* `safe.bareRepository=explicit` (which refuses cwd-based discovery of
|
|
149
|
+
* bare repos but expressly allows `--git-dir` — see
|
|
150
|
+
* GitManager.bareRemoteHasBranch for the same pattern).
|
|
151
|
+
*
|
|
152
|
+
* Deliberately omits `--quiet`: simple-git only treats a task as failed
|
|
153
|
+
* when the process both exits non-zero AND writes to stderr
|
|
154
|
+
* (isTaskError), so a quiet, silent-on-failure `--verify` would leave a
|
|
155
|
+
* missing branch indistinguishable from success. Without `--quiet`,
|
|
156
|
+
* `rev-parse --verify` writes its "fatal: ..." to stderr on failure, which
|
|
157
|
+
* is what makes simple-git reject the promise here.
|
|
158
|
+
*/
|
|
159
|
+
private verifyBaseBranchExists;
|
|
142
160
|
/**
|
|
143
161
|
* Ensure remote.git bare repo exists.
|
|
144
162
|
* On first run, clone from GitHub as a bare repo.
|
|
163
|
+
*
|
|
164
|
+
* Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
|
|
165
|
+
* commits, or a base branch that's never been pushed) exits 0 and produces
|
|
166
|
+
* a refs-less bare repo — HEAD points at an unborn branch. `fs.stat`
|
|
167
|
+
* cannot distinguish this from a healthy clone, so left unchecked it
|
|
168
|
+
* silently poisons remote.git: every later branch operation (Lambda-side
|
|
169
|
+
* clone provisioning, worker pushes) breaks, and the fs.stat short-circuit
|
|
170
|
+
* means it never heals on its own. We verify the base branch exists right
|
|
171
|
+
* after cloning and again on the already-exists fast path, since a
|
|
172
|
+
* previous run could have left a poisoned remote.git behind before this
|
|
173
|
+
* guard existed.
|
|
145
174
|
*/
|
|
146
175
|
private ensureRemoteGit;
|
|
147
176
|
/**
|
|
@@ -180,6 +209,41 @@ export declare class CmsWorker {
|
|
|
180
209
|
*/
|
|
181
210
|
private pushSettingsBranches;
|
|
182
211
|
syncGit(): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Fast-forward the base branch's own working-tree clone
|
|
214
|
+
* (content-branches/<baseBranch>) to match origin/<baseBranch>.
|
|
215
|
+
*
|
|
216
|
+
* Previously this clone was refreshed only incidentally, by the generic
|
|
217
|
+
* rebase loop below (rebaseActiveBranches): for a branch with status
|
|
218
|
+
* 'editing', rebasing onto origin/<baseBranch> degenerates to a
|
|
219
|
+
* fast-forward when the clone IS the base branch. But that loop's skip
|
|
220
|
+
* paths -- a dirty tree, a missing .git -- are silent, which is the
|
|
221
|
+
* suspected live failure mode: a wedged base clone with no diagnosable
|
|
222
|
+
* signal in the logs. This dedicated step makes the refresh explicit,
|
|
223
|
+
* ff-only, and loud, so a stuck base view (an editor forking a new branch
|
|
224
|
+
* "from base" that's actually a stale snapshot) is diagnosable from logs.
|
|
225
|
+
* This runs every sync cycle so the drift window is bounded by
|
|
226
|
+
* gitSyncInterval.
|
|
227
|
+
*
|
|
228
|
+
* ff-only on purpose: this clone must stay a linear mirror of
|
|
229
|
+
* origin/<baseBranch>, so a merge that isn't a fast-forward (diverged
|
|
230
|
+
* local history) is treated as a should-never-happen condition and left
|
|
231
|
+
* untouched rather than force-resolved.
|
|
232
|
+
*/
|
|
233
|
+
private refreshBaseBranchWorkspace;
|
|
234
|
+
/**
|
|
235
|
+
* Poll GitHub for a submitted/approved branch's PR resolution.
|
|
236
|
+
*
|
|
237
|
+
* submitted/approved branches sit outside the rebase loop and get no
|
|
238
|
+
* other signal that their PR resolved on GitHub -- nothing pushes a
|
|
239
|
+
* merge/close webhook back into the branch workspace. merged ->
|
|
240
|
+
* auto-archive via buildMergedBranchUpdate (shared with the manual
|
|
241
|
+
* markAsMerged API so both paths produce identical archived-branch
|
|
242
|
+
* metadata). closed-without-merge -> record pullRequestState only; an
|
|
243
|
+
* admin decides the workflow transition from there. Best-effort: any
|
|
244
|
+
* failure here is logged and swallowed, retried next sync cycle.
|
|
245
|
+
*/
|
|
246
|
+
private pollMergeState;
|
|
183
247
|
private rebaseActiveBranches;
|
|
184
248
|
refreshAuthCache(): Promise<void>;
|
|
185
249
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-worker.d.ts","sourceRoot":"","sources":["../../src/worker/cms-worker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cms-worker.d.ts","sourceRoot":"","sources":["../../src/worker/cms-worker.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;AAEpD,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAA;IACrB,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,kBAAkB,CAAA;IACrC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uDAAuD;IACvD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kEAAkE;IAClE,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAMD;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAEhD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAO5D;AA2DD;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;IAwBR,OAAO,CAAC,MAAM;IAvB1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,UAAU,CAAQ;IAM1B,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,GAAG,CAAqB;gBAEZ,MAAM,EAAE,eAAe;IAerC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;YACW,WAAW;YAyBX,WAAW;IAWzB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAkBpB;;;;;;;;;;;;;;;OAeG;YACW,sBAAsB;IAUpC;;;;;;;;;;;;;;OAcG;YACW,eAAe;IAiD7B;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCvC;;;;;;;;OAQG;YACW,sBAAsB;YAqBtB,WAAW;IAkHzB;;;OAGG;YACW,oBAAoB;IA2ClC;;;OAGG;YACW,6BAA6B;IAsB3C,OAAO,CAAC,cAAc;YAIR,kBAAkB;IAahC;;;OAGG;YACW,oBAAoB;IAqB5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B9B;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,0BAA0B;IAgHxC;;;;;;;;;;;OAWG;YACW,cAAc;YAkDd,oBAAoB;IA6O5B,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CAYxC"}
|
|
@@ -4,10 +4,11 @@ import { simpleGit } from 'simple-git';
|
|
|
4
4
|
import lockfile from 'proper-lockfile';
|
|
5
5
|
import { dequeueTask, completeTask, failTask, retryTask, recoverOrphanedTasks, cleanupOldTasks, cmsTaskQueueLogger, } from './task-queue.js';
|
|
6
6
|
import { createOrUpdatePullRequest, createCanopyOctokit } from '../github-service.js';
|
|
7
|
-
import { getBranchMetadataFileManager, BranchMetadataFileManager } from '../branch-metadata.js';
|
|
7
|
+
import { getBranchMetadataFileManager, BranchMetadataFileManager, buildMergedBranchUpdate, } from '../branch-metadata.js';
|
|
8
8
|
import { extractIdFromFilename } from '../content-id-index.js';
|
|
9
9
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
10
10
|
import { ROOT_COLLECTION_ID } from '../paths/types.js';
|
|
11
|
+
import { sanitizeBranchName } from '../paths/branch.js';
|
|
11
12
|
import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
12
13
|
const DEFAULT_TASK_TIMEOUT = 60_000;
|
|
13
14
|
const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -135,6 +136,7 @@ export class CmsWorker {
|
|
|
135
136
|
this.remoteGitPath = path.join(config.workspacePath, 'remote.git');
|
|
136
137
|
this.contentBranchesPath = path.join(config.workspacePath, 'content-branches');
|
|
137
138
|
this.baseBranch = config.baseBranch ?? 'main';
|
|
139
|
+
this.sanitizedBaseBranch = sanitizeBranchName(this.baseBranch);
|
|
138
140
|
this.maxTasksPerCycle = config.maxTasksPerCycle ?? 10;
|
|
139
141
|
this.taskTimeoutMs = config.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT;
|
|
140
142
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -147,19 +149,37 @@ export class CmsWorker {
|
|
|
147
149
|
console.log('CMS Worker starting...');
|
|
148
150
|
// Acquire lock to prevent concurrent workers
|
|
149
151
|
await this.acquireLock();
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
// Everything below runs while holding the cross-host worker lock. A
|
|
153
|
+
// failure here (most notably the empty-remote guard inside
|
|
154
|
+
// ensureRemoteGit) means the process is about to exit, and systemd
|
|
155
|
+
// (Restart=always) will retry — but a still-held lock would make every
|
|
156
|
+
// retry fail with ELOCKED for up to lockStaleMs. Release before
|
|
157
|
+
// rethrowing so the next start() (this process's retry, or another host)
|
|
158
|
+
// can acquire immediately. We deliberately do NOT reorder ensureRemoteGit
|
|
159
|
+
// ahead of acquireLock: two hosts cold-starting at once would then both
|
|
160
|
+
// race to `git clone --bare` into the same remoteGitPath (the same class
|
|
161
|
+
// of race that acquireProvisioningLock guards against for workspace
|
|
162
|
+
// clones elsewhere) — acquiring the lock first is what already
|
|
163
|
+
// serializes that.
|
|
164
|
+
try {
|
|
165
|
+
// Ensure remote.git exists (init bare repo if first run)
|
|
166
|
+
await this.ensureRemoteGit();
|
|
167
|
+
// Recover any orphaned tasks from a previous crash
|
|
168
|
+
const recovered = await recoverOrphanedTasks(this.taskDir, undefined, this.log);
|
|
169
|
+
if (recovered > 0) {
|
|
170
|
+
console.log(`Recovered ${recovered} orphaned task(s)`);
|
|
171
|
+
}
|
|
172
|
+
// Run initial sync + cache refresh immediately
|
|
173
|
+
const initialTasks = [this.syncGit()];
|
|
174
|
+
if (this.config.refreshAuthCache) {
|
|
175
|
+
initialTasks.push(this.refreshAuthCache());
|
|
176
|
+
}
|
|
177
|
+
await Promise.allSettled(initialTasks);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
await this.releaseLock();
|
|
181
|
+
throw err;
|
|
161
182
|
}
|
|
162
|
-
await Promise.allSettled(initialTasks);
|
|
163
183
|
// Start recurring task loops using setTimeout chaining
|
|
164
184
|
// (avoids setInterval overlap when tasks take longer than the interval)
|
|
165
185
|
const taskInterval = this.config.taskPollInterval ?? 5_000;
|
|
@@ -276,24 +296,86 @@ export class CmsWorker {
|
|
|
276
296
|
};
|
|
277
297
|
run();
|
|
278
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
|
|
301
|
+
*
|
|
302
|
+
* Uses an explicit `--git-dir` invocation rather than `simpleGit({ baseDir
|
|
303
|
+
* })` so this also works in sandboxed/CI git environments that set
|
|
304
|
+
* `safe.bareRepository=explicit` (which refuses cwd-based discovery of
|
|
305
|
+
* bare repos but expressly allows `--git-dir` — see
|
|
306
|
+
* GitManager.bareRemoteHasBranch for the same pattern).
|
|
307
|
+
*
|
|
308
|
+
* Deliberately omits `--quiet`: simple-git only treats a task as failed
|
|
309
|
+
* when the process both exits non-zero AND writes to stderr
|
|
310
|
+
* (isTaskError), so a quiet, silent-on-failure `--verify` would leave a
|
|
311
|
+
* missing branch indistinguishable from success. Without `--quiet`,
|
|
312
|
+
* `rev-parse --verify` writes its "fatal: ..." to stderr on failure, which
|
|
313
|
+
* is what makes simple-git reject the promise here.
|
|
314
|
+
*/
|
|
315
|
+
async verifyBaseBranchExists(gitDir) {
|
|
316
|
+
await simpleGit().raw([
|
|
317
|
+
'--git-dir',
|
|
318
|
+
gitDir,
|
|
319
|
+
'rev-parse',
|
|
320
|
+
'--verify',
|
|
321
|
+
`refs/heads/${this.baseBranch}`,
|
|
322
|
+
]);
|
|
323
|
+
}
|
|
279
324
|
/**
|
|
280
325
|
* Ensure remote.git bare repo exists.
|
|
281
326
|
* On first run, clone from GitHub as a bare repo.
|
|
327
|
+
*
|
|
328
|
+
* Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
|
|
329
|
+
* commits, or a base branch that's never been pushed) exits 0 and produces
|
|
330
|
+
* a refs-less bare repo — HEAD points at an unborn branch. `fs.stat`
|
|
331
|
+
* cannot distinguish this from a healthy clone, so left unchecked it
|
|
332
|
+
* silently poisons remote.git: every later branch operation (Lambda-side
|
|
333
|
+
* clone provisioning, worker pushes) breaks, and the fs.stat short-circuit
|
|
334
|
+
* means it never heals on its own. We verify the base branch exists right
|
|
335
|
+
* after cloning and again on the already-exists fast path, since a
|
|
336
|
+
* previous run could have left a poisoned remote.git behind before this
|
|
337
|
+
* guard existed.
|
|
282
338
|
*/
|
|
283
339
|
async ensureRemoteGit() {
|
|
340
|
+
let exists;
|
|
284
341
|
try {
|
|
285
342
|
await fs.stat(this.remoteGitPath);
|
|
286
|
-
|
|
343
|
+
exists = true;
|
|
287
344
|
}
|
|
288
345
|
catch {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
346
|
+
exists = false;
|
|
347
|
+
}
|
|
348
|
+
if (exists) {
|
|
349
|
+
try {
|
|
350
|
+
await this.verifyBaseBranchExists(this.remoteGitPath);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
console.error(`remote.git base branch verification failed: ${getErrorMessage(err)}`);
|
|
354
|
+
// Do NOT auto-delete: an existing remote.git could hold unpushed
|
|
355
|
+
// canopycms-settings-* branches or other state worth preserving.
|
|
356
|
+
// Deletion here is the operator's call, not ours.
|
|
357
|
+
throw new Error(`remote.git at ${this.remoteGitPath} has no branch '${this.baseBranch}' (likely cloned while the GitHub repo was empty). Delete ${this.remoteGitPath} and restart the worker to re-clone.`);
|
|
358
|
+
}
|
|
359
|
+
return; // Already exists and has the base branch
|
|
360
|
+
}
|
|
361
|
+
console.log('Initializing remote.git from GitHub...');
|
|
362
|
+
const git = simpleGit();
|
|
363
|
+
await git.clone(this.buildGitHubUrl(), this.remoteGitPath, ['--bare']);
|
|
364
|
+
try {
|
|
365
|
+
await this.verifyBaseBranchExists(this.remoteGitPath);
|
|
296
366
|
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
console.error(`remote.git base branch verification failed after clone: ${getErrorMessage(err)}`);
|
|
369
|
+
// Deleting before throwing is what makes this recoverable: the next
|
|
370
|
+
// start() sees no remote.git and re-clones, instead of being stuck
|
|
371
|
+
// forever behind a poisoned bare repo that fs.stat alone can't detect.
|
|
372
|
+
await fs.rm(this.remoteGitPath, { recursive: true, force: true });
|
|
373
|
+
throw new Error(`remote.git clone of ${this.config.githubOwner}/${this.config.githubRepo} has no branch '${this.baseBranch}' - the GitHub repository is empty or the base branch does not exist. Push an initial commit to '${this.baseBranch}' and restart the worker (systemd will retry automatically).`);
|
|
374
|
+
}
|
|
375
|
+
// Remove the origin remote so the token doesn't persist in config
|
|
376
|
+
const bareGit = simpleGit({ baseDir: this.remoteGitPath });
|
|
377
|
+
await bareGit.removeRemote('origin').catch(() => { });
|
|
378
|
+
console.log('remote.git initialized');
|
|
297
379
|
}
|
|
298
380
|
/**
|
|
299
381
|
* Process queued tasks from Lambda.
|
|
@@ -492,8 +574,21 @@ export class CmsWorker {
|
|
|
492
574
|
};
|
|
493
575
|
if (result.prUrl)
|
|
494
576
|
updates.pullRequestUrl = result.prUrl;
|
|
495
|
-
if (result.prNumber)
|
|
577
|
+
if (result.prNumber) {
|
|
496
578
|
updates.pullRequestNumber = result.prNumber;
|
|
579
|
+
// As soon as the PR exists the field should read 'open' -- without
|
|
580
|
+
// this it would stay absent until the next poll cycle observes it.
|
|
581
|
+
// Exception: the git-sync loop may have archived this branch (PR
|
|
582
|
+
// merged/closed) while this task was in flight; a late task
|
|
583
|
+
// completion must not downgrade that terminal state back to 'open'.
|
|
584
|
+
const current = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
585
|
+
const terminal = current?.branch.status === 'archived' ||
|
|
586
|
+
current?.branch.pullRequestState === 'merged' ||
|
|
587
|
+
current?.branch.pullRequestState === 'closed';
|
|
588
|
+
if (!terminal) {
|
|
589
|
+
updates.pullRequestState = 'open';
|
|
590
|
+
}
|
|
591
|
+
}
|
|
497
592
|
await meta.save({ branch: updates });
|
|
498
593
|
}
|
|
499
594
|
catch (err) {
|
|
@@ -580,10 +675,178 @@ export class CmsWorker {
|
|
|
580
675
|
// Push settings branches to GitHub (belt-and-suspenders for task queue).
|
|
581
676
|
// Ensures settings reach GitHub even if a task queue entry is lost.
|
|
582
677
|
await this.pushSettingsBranches(git);
|
|
678
|
+
await this.refreshBaseBranchWorkspace();
|
|
583
679
|
await this.rebaseActiveBranches();
|
|
584
680
|
// Periodically clean up old completed/failed tasks
|
|
585
681
|
await cleanupOldTasks(this.taskDir, undefined, this.log);
|
|
586
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Fast-forward the base branch's own working-tree clone
|
|
685
|
+
* (content-branches/<baseBranch>) to match origin/<baseBranch>.
|
|
686
|
+
*
|
|
687
|
+
* Previously this clone was refreshed only incidentally, by the generic
|
|
688
|
+
* rebase loop below (rebaseActiveBranches): for a branch with status
|
|
689
|
+
* 'editing', rebasing onto origin/<baseBranch> degenerates to a
|
|
690
|
+
* fast-forward when the clone IS the base branch. But that loop's skip
|
|
691
|
+
* paths -- a dirty tree, a missing .git -- are silent, which is the
|
|
692
|
+
* suspected live failure mode: a wedged base clone with no diagnosable
|
|
693
|
+
* signal in the logs. This dedicated step makes the refresh explicit,
|
|
694
|
+
* ff-only, and loud, so a stuck base view (an editor forking a new branch
|
|
695
|
+
* "from base" that's actually a stale snapshot) is diagnosable from logs.
|
|
696
|
+
* This runs every sync cycle so the drift window is bounded by
|
|
697
|
+
* gitSyncInterval.
|
|
698
|
+
*
|
|
699
|
+
* ff-only on purpose: this clone must stay a linear mirror of
|
|
700
|
+
* origin/<baseBranch>, so a merge that isn't a fast-forward (diverged
|
|
701
|
+
* local history) is treated as a should-never-happen condition and left
|
|
702
|
+
* untouched rather than force-resolved.
|
|
703
|
+
*/
|
|
704
|
+
async refreshBaseBranchWorkspace() {
|
|
705
|
+
// Sanitized name for the workspace directory (a base branch containing
|
|
706
|
+
// e.g. '/' would otherwise stat a wrong nested path here forever).
|
|
707
|
+
const basePath = path.join(this.contentBranchesPath, this.sanitizedBaseBranch);
|
|
708
|
+
const gitDir = path.join(basePath, '.git');
|
|
709
|
+
try {
|
|
710
|
+
let gitDirStat;
|
|
711
|
+
try {
|
|
712
|
+
gitDirStat = await fs.stat(gitDir);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
gitDirStat = null;
|
|
716
|
+
}
|
|
717
|
+
if (!gitDirStat || !gitDirStat.isDirectory()) {
|
|
718
|
+
console.log(`Base branch workspace (${this.baseBranch}): not yet provisioned, skipping refresh`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const baseGit = simpleGit({
|
|
722
|
+
baseDir: basePath,
|
|
723
|
+
// Keep git non-interactive during the merge so it never blocks on an
|
|
724
|
+
// editor. simple-git >=3.32 blocks setting core.editor unless
|
|
725
|
+
// explicitly opted in; the value here is a hardcoded literal
|
|
726
|
+
// ("true", the shell no-op), not user input, so enabling
|
|
727
|
+
// allowUnsafeEditor carries no injection risk (same as the rebase
|
|
728
|
+
// loop's git config below).
|
|
729
|
+
config: ['core.editor=true'],
|
|
730
|
+
unsafe: { allowUnsafeEditor: true },
|
|
731
|
+
// DEP-H1: a hung fetch/merge against this EFS-backed clone would
|
|
732
|
+
// stall the sync loop forever (scheduleLoop only reschedules after
|
|
733
|
+
// completion). The block timeout is inactivity-based, so a
|
|
734
|
+
// slow-but-flowing transfer is unaffected -- same as syncGit()'s
|
|
735
|
+
// remote handle.
|
|
736
|
+
timeout: { block: this.taskTimeoutMs },
|
|
737
|
+
});
|
|
738
|
+
// Nothing makes this clone read-only. A direct edit here (or a stray
|
|
739
|
+
// process) wedges every editor's view of the base branch until an
|
|
740
|
+
// operator intervenes, so a dirty tree is loud, not a quiet skip.
|
|
741
|
+
// Only TRACKED changes block the refresh: a stray untracked file (e.g.
|
|
742
|
+
// runtime metadata missing from .git/info/exclude) must not wedge the
|
|
743
|
+
// fast-forward forever — if an untracked file would collide with
|
|
744
|
+
// incoming content, the --ff-only merge below refuses on its own and
|
|
745
|
+
// that failure is already logged loudly.
|
|
746
|
+
const status = await baseGit.status();
|
|
747
|
+
const trackedDirty = status.files.filter((f) => f.index !== '?' || f.working_dir !== '?');
|
|
748
|
+
if (trackedDirty.length > 0) {
|
|
749
|
+
console.error(`Base branch workspace (${this.baseBranch}) has uncommitted changes -- skipping refresh. Dirty files: ${trackedDirty.map((f) => f.path).join(', ')}`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
// Raw (unsanitized) name from here on: these are git ref operations
|
|
753
|
+
// against origin/<baseBranch>, not filesystem paths, so they must use
|
|
754
|
+
// the same name GitHub knows the branch by.
|
|
755
|
+
await baseGit.fetch('origin', this.baseBranch);
|
|
756
|
+
// Use rev-list instead of status.behind for the same reason as the
|
|
757
|
+
// rebase loop below: status.behind only works with an upstream
|
|
758
|
+
// tracking branch configured, which isn't guaranteed here.
|
|
759
|
+
const behindCount = parseInt((await baseGit.raw(['rev-list', '--count', `HEAD..origin/${this.baseBranch}`])).trim(), 10);
|
|
760
|
+
if (behindCount > 0) {
|
|
761
|
+
try {
|
|
762
|
+
await baseGit.merge(['--ff-only', `origin/${this.baseBranch}`]);
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
console.error(`Base branch workspace (${this.baseBranch}) failed to fast-forward (diverged local history?): ${getErrorMessage(err)}`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
await invalidateBranchContentCaches(basePath);
|
|
769
|
+
}
|
|
770
|
+
// Hygiene: conflictStatus/conflictFiles are meaningless for the base
|
|
771
|
+
// branch's own metadata and may be left over from before the base was
|
|
772
|
+
// excluded from rebaseActiveBranches()'s conflict-resolution loop.
|
|
773
|
+
// Reuse the already-clean no-op-save guard from that loop: save()
|
|
774
|
+
// eager-regenerates the branch registry (O(branch count) EFS reads),
|
|
775
|
+
// so skip it when there's nothing to clear.
|
|
776
|
+
const currentMeta = await BranchMetadataFileManager.loadOnly(basePath);
|
|
777
|
+
const conflictStatus = currentMeta?.branch.conflictStatus;
|
|
778
|
+
const conflictFiles = currentMeta?.branch.conflictFiles;
|
|
779
|
+
const alreadyClean = (conflictStatus === undefined || conflictStatus === 'clean') &&
|
|
780
|
+
(conflictFiles === undefined || conflictFiles.length === 0);
|
|
781
|
+
if (!alreadyClean) {
|
|
782
|
+
const meta = getBranchMetadataFileManager(basePath, this.contentBranchesPath);
|
|
783
|
+
await meta.save({
|
|
784
|
+
branch: { name: this.baseBranch, conflictStatus: 'clean', conflictFiles: [] },
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
// One concise per-cycle line -- the diagnostic for the next live deploy.
|
|
788
|
+
console.log(behindCount > 0
|
|
789
|
+
? `Base branch workspace (${this.baseBranch}): fast-forwarded ${behindCount} commit(s)`
|
|
790
|
+
: `Base branch workspace (${this.baseBranch}): up to date`);
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
console.error(`Base branch workspace (${this.baseBranch}) refresh failed: ${getErrorMessage(err)}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Poll GitHub for a submitted/approved branch's PR resolution.
|
|
798
|
+
*
|
|
799
|
+
* submitted/approved branches sit outside the rebase loop and get no
|
|
800
|
+
* other signal that their PR resolved on GitHub -- nothing pushes a
|
|
801
|
+
* merge/close webhook back into the branch workspace. merged ->
|
|
802
|
+
* auto-archive via buildMergedBranchUpdate (shared with the manual
|
|
803
|
+
* markAsMerged API so both paths produce identical archived-branch
|
|
804
|
+
* metadata). closed-without-merge -> record pullRequestState only; an
|
|
805
|
+
* admin decides the workflow transition from there. Best-effort: any
|
|
806
|
+
* failure here is logged and swallowed, retried next sync cycle.
|
|
807
|
+
*/
|
|
808
|
+
async pollMergeState(branchDir, branchPath, metaFile) {
|
|
809
|
+
const prNumber = metaFile?.branch.pullRequestNumber;
|
|
810
|
+
if (!prNumber)
|
|
811
|
+
return;
|
|
812
|
+
const controller = new AbortController();
|
|
813
|
+
const timer = setTimeout(() => controller.abort(), this.taskTimeoutMs);
|
|
814
|
+
try {
|
|
815
|
+
const { data } = await this.octokit.pulls.get({
|
|
816
|
+
owner: this.config.githubOwner,
|
|
817
|
+
repo: this.config.githubRepo,
|
|
818
|
+
pull_number: prNumber,
|
|
819
|
+
request: { signal: controller.signal },
|
|
820
|
+
});
|
|
821
|
+
if (data.merged) {
|
|
822
|
+
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath);
|
|
823
|
+
// Use GitHub's actual merge time when available; buildMergedBranchUpdate
|
|
824
|
+
// falls back to "now" (its default `now` param) when merged_at is absent.
|
|
825
|
+
await meta.save({
|
|
826
|
+
branch: buildMergedBranchUpdate(branchDir, data.merged_at ? new Date(data.merged_at) : undefined),
|
|
827
|
+
});
|
|
828
|
+
console.log(` PR #${prNumber} for ${branchDir} is merged -> archived`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const newState = data.state === 'closed' ? 'closed' : 'open';
|
|
832
|
+
// Re-load fresh (not the loop-top `metaFile` snapshot passed in) -- a
|
|
833
|
+
// concurrent Lambda write (e.g. an editor re-submitting) may have
|
|
834
|
+
// landed since that snapshot was taken.
|
|
835
|
+
const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
836
|
+
if (currentMeta?.branch.pullRequestState === newState)
|
|
837
|
+
return;
|
|
838
|
+
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath);
|
|
839
|
+
await meta.save({ branch: { name: branchDir, pullRequestState: newState } });
|
|
840
|
+
console.log(` PR #${prNumber} for ${branchDir}: pullRequestState -> ${newState}`);
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
// Non-fatal: transient GitHub/network errors are retried next cycle.
|
|
844
|
+
console.warn(` Failed to poll PR #${prNumber} for ${branchDir}: ${getErrorMessage(err)}`);
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
587
850
|
async rebaseActiveBranches() {
|
|
588
851
|
let branchDirs;
|
|
589
852
|
try {
|
|
@@ -593,14 +856,35 @@ export class CmsWorker {
|
|
|
593
856
|
return;
|
|
594
857
|
}
|
|
595
858
|
for (const branchDir of branchDirs) {
|
|
859
|
+
// Known structural entries under content-branches/, not branch
|
|
860
|
+
// workspaces: branches.json (registry snapshot) and dot-prefixed
|
|
861
|
+
// entries (.canopy-meta/, transient lock dirs). Skip them silently so
|
|
862
|
+
// the no-.git skip logs below don't fire for them every cycle.
|
|
863
|
+
// Everything else still logs loudly on skip.
|
|
864
|
+
if (branchDir.startsWith('.') || branchDir === 'branches.json') {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
596
867
|
const branchPath = path.join(this.contentBranchesPath, branchDir);
|
|
597
868
|
const gitDir = path.join(branchPath, '.git');
|
|
598
869
|
try {
|
|
599
870
|
const stat = await fs.stat(gitDir);
|
|
600
|
-
if (!stat.isDirectory())
|
|
871
|
+
if (!stat.isDirectory()) {
|
|
872
|
+
console.log(` Skipping ${branchDir}: .git is not a directory`);
|
|
601
873
|
continue;
|
|
874
|
+
}
|
|
602
875
|
}
|
|
603
876
|
catch {
|
|
877
|
+
console.log(` Skipping ${branchDir}: no .git directory (not a branch workspace)`);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
// The base branch's own clone is refreshed ff-only by
|
|
881
|
+
// refreshBaseBranchWorkspace() earlier in syncGit(). Routing it
|
|
882
|
+
// through this conflict-resolution rebase loop could rewrite its
|
|
883
|
+
// history (the --theirs loop below) and stamp meaningless conflict
|
|
884
|
+
// metadata on it. Compare sanitized-vs-sanitized: branchDir is a
|
|
885
|
+
// filesystem name (already sanitized), this.baseBranch is raw.
|
|
886
|
+
if (branchDir === this.sanitizedBaseBranch) {
|
|
887
|
+
console.log(` Skipping ${branchDir}: base branch (refreshed separately)`);
|
|
604
888
|
continue;
|
|
605
889
|
}
|
|
606
890
|
try {
|
|
@@ -608,11 +892,17 @@ export class CmsWorker {
|
|
|
608
892
|
const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
609
893
|
const branchStatus = metaFile?.branch.status;
|
|
610
894
|
// Skip branches that shouldn't be mutated:
|
|
611
|
-
// - submitted/approved: in review, don't rewrite history under an
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
895
|
+
// - submitted/approved: in review, don't rewrite history under an
|
|
896
|
+
// active PR -- but do poll GitHub for the PR's resolution, since
|
|
897
|
+
// nothing else tells the worker a merge/close happened.
|
|
898
|
+
// - archived: already merged, no reason to rebase and no PR left to
|
|
899
|
+
// poll (avoid the wasted API call).
|
|
900
|
+
if (branchStatus === 'submitted' || branchStatus === 'approved') {
|
|
901
|
+
console.log(` Skipping ${branchDir} (${branchStatus})`);
|
|
902
|
+
await this.pollMergeState(branchDir, branchPath, metaFile);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (branchStatus === 'archived') {
|
|
616
906
|
console.log(` Skipping ${branchDir} (${branchStatus})`);
|
|
617
907
|
continue;
|
|
618
908
|
}
|