canopycms-cdk 0.0.67-int.90 → 0.0.67
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/constructs/asset-support.d.ts +137 -269
- package/dist/constructs/asset-support.js +275 -455
- package/dist/constructs/cms-distribution.d.ts +23 -40
- package/dist/constructs/cms-distribution.js +61 -88
- package/dist/constructs/cms-service.d.ts +38 -60
- package/dist/constructs/cms-service.js +246 -390
- package/dist/constructs/lambda-execution-role.d.ts +23 -39
- package/dist/constructs/lambda-execution-role.js +37 -60
- package/dist/index.js +0 -2
- package/dist/worker.d.ts +0 -2
- package/dist/worker.js +0 -2
- package/lambda/asset-transform/dist/handler.js +33 -49
- package/lambda/asset-transform/dist/node_modules/.package-lock.json +38 -38
- package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/README.md +1 -1
- package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/glib-2.0/include/glibconfig.h +2 -2
- package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/{libvips-cpp.so.8.18.6 → libvips-cpp.so.8.18.7} +0 -0
- package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/package.json +2 -2
- package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/versions.json +11 -11
- package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/index.cjs +1 -1
- package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/lib/{sharp-linux-arm64-0.35.4.node → sharp-linux-arm64-0.35.5.node} +0 -0
- package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/package.json +2 -2
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/index.cjs +1 -1
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.5.node.js +1 -0
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/{sharp-wasm32-0.35.4.node.wasm → sharp-wasm32-0.35.5.node.wasm} +0 -0
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/package.json +1 -1
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/versions.json +7 -7
- package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.cts +2 -2
- package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.mts +3 -3
- package/lambda/asset-transform/dist/node_modules/sharp/dist/is.cjs +8 -2
- package/lambda/asset-transform/dist/node_modules/sharp/dist/is.mjs +8 -2
- package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.cjs +2 -2
- package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.mjs +2 -2
- package/lambda/asset-transform/dist/node_modules/sharp/dist/output.cjs +1 -0
- package/lambda/asset-transform/dist/node_modules/sharp/dist/output.mjs +1 -0
- package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.cjs +14 -14
- package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.mjs +14 -14
- package/lambda/asset-transform/dist/node_modules/sharp/dist/sharp.cjs +3 -3
- package/lambda/asset-transform/dist/node_modules/sharp/dist/sharp.mjs +3 -3
- package/lambda/asset-transform/dist/node_modules/sharp/lib/index.d.ts +2 -2
- package/lambda/asset-transform/dist/node_modules/sharp/package.json +35 -35
- package/lambda/asset-transform/dist/node_modules/sharp/src/binding.gyp +1 -1
- package/lambda/asset-transform/dist/node_modules/sharp/src/common.cc +4 -4
- package/lambda/asset-transform/dist/node_modules/sharp/src/common.h +2 -2
- package/lambda/asset-transform/dist/node_modules/sharp/src/pipeline.cc +31 -6
- package/lambda/asset-transform/dist/node_modules/sharp/src/utilities.cc +1 -1
- package/package.json +4 -4
- package/worker/dist/index.js +251 -364
- package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.4.node.js +0 -1
package/worker/dist/index.js
CHANGED
|
@@ -12515,18 +12515,18 @@ var init_config2 = __esm({
|
|
|
12515
12515
|
onAccountClick: external_exports.function().returns(external_exports.void()).optional(),
|
|
12516
12516
|
onLogoutClick: external_exports.function().returns(external_exports.void()).optional(),
|
|
12517
12517
|
// Optional: custom account component (e.g., Clerk's UserButton)
|
|
12518
|
-
AccountComponent: external_exports.custom().optional()
|
|
12518
|
+
AccountComponent: external_exports.custom().optional(),
|
|
12519
|
+
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
12520
|
+
SignInComponent: external_exports.custom().optional()
|
|
12519
12521
|
});
|
|
12520
12522
|
CanopyConfigSchema = external_exports.object({
|
|
12521
12523
|
media: mediaSchema.optional(),
|
|
12522
|
-
// No outer .optional()
|
|
12523
|
-
//
|
|
12524
|
-
//
|
|
12525
|
-
//
|
|
12526
|
-
//
|
|
12527
|
-
//
|
|
12528
|
-
// still resolves to 'deny' and an omitted level inside the object form stays undefined
|
|
12529
|
-
// (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
12524
|
+
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
12525
|
+
// input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
|
|
12526
|
+
// .optional() would short-circuit before the inner default runs, defeating the fail-closed
|
|
12527
|
+
// default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
|
|
12528
|
+
// inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
|
|
12529
|
+
// undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
12530
12530
|
defaultBranchAccess: defaultBranchAccessSchema,
|
|
12531
12531
|
defaultPathAccess: defaultPathAccessSchema,
|
|
12532
12532
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
@@ -12550,16 +12550,11 @@ var init_config2 = __esm({
|
|
|
12550
12550
|
allowNetworkRemoteInProd: external_exports.boolean().optional(),
|
|
12551
12551
|
settingsBranch: external_exports.string().optional(),
|
|
12552
12552
|
autoCreateSettingsPR: external_exports.boolean().optional(),
|
|
12553
|
-
//
|
|
12554
|
-
//
|
|
12555
|
-
//
|
|
12556
|
-
//
|
|
12557
|
-
//
|
|
12558
|
-
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
12559
|
-
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
12560
|
-
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
12561
|
-
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
12562
|
-
// Do not "fix" this by removing `.optional()`.
|
|
12553
|
+
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
12554
|
+
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
12555
|
+
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
12556
|
+
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
12557
|
+
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
12563
12558
|
deploymentName: deploymentNameSchema.optional(),
|
|
12564
12559
|
contentRoot: contentRootSchema.default("content"),
|
|
12565
12560
|
sourceRoot: sourceRootSchema.optional(),
|
|
@@ -48211,15 +48206,11 @@ var DebugLogger = class {
|
|
|
48211
48206
|
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
48212
48207
|
}
|
|
48213
48208
|
/**
|
|
48214
|
-
* The ISO-8601 timestamp leads the line BARE
|
|
48215
|
-
*
|
|
48216
|
-
*
|
|
48217
|
-
*
|
|
48218
|
-
*
|
|
48219
|
-
* leading `[` meant every debug line was silently folded into the previous
|
|
48220
|
-
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
48221
|
-
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
48222
|
-
* apart from those two characters.
|
|
48209
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
48210
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
48211
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
48212
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
48213
|
+
* character folds the line into the previous event instead of starting its own.
|
|
48223
48214
|
*/
|
|
48224
48215
|
formatMessage(level, category, message) {
|
|
48225
48216
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -48252,15 +48243,9 @@ var DebugLogger = class {
|
|
|
48252
48243
|
throw new Error(errorMsg);
|
|
48253
48244
|
}
|
|
48254
48245
|
}
|
|
48255
|
-
/**
|
|
48256
|
-
* Start timing an operation
|
|
48257
|
-
*/
|
|
48258
48246
|
time(label) {
|
|
48259
48247
|
this.timers.set(label, Date.now());
|
|
48260
48248
|
}
|
|
48261
|
-
/**
|
|
48262
|
-
* End timing an operation and log the duration
|
|
48263
|
-
*/
|
|
48264
48249
|
timeEnd(category, label) {
|
|
48265
48250
|
const start = this.timers.get(label);
|
|
48266
48251
|
if (start === void 0) {
|
|
@@ -48272,9 +48257,6 @@ var DebugLogger = class {
|
|
|
48272
48257
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
48273
48258
|
return duration;
|
|
48274
48259
|
}
|
|
48275
|
-
/**
|
|
48276
|
-
* Wrap an async function with automatic timing
|
|
48277
|
-
*/
|
|
48278
48260
|
async timed(category, label, fn) {
|
|
48279
48261
|
this.time(label);
|
|
48280
48262
|
try {
|
|
@@ -48614,7 +48596,7 @@ async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger3) {
|
|
|
48614
48596
|
}
|
|
48615
48597
|
}
|
|
48616
48598
|
|
|
48617
|
-
// ../canopycms/src/
|
|
48599
|
+
// ../canopycms/src/task-queue/cms-task-queue.ts
|
|
48618
48600
|
var debugLogger = createDebugLogger({ prefix: "TaskQueue" });
|
|
48619
48601
|
var cmsTaskQueueLogger = {
|
|
48620
48602
|
debug(message, data2) {
|
|
@@ -48915,7 +48897,7 @@ var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
|
48915
48897
|
// ../canopycms/src/worker/cms-worker.ts
|
|
48916
48898
|
init_deployment_name();
|
|
48917
48899
|
|
|
48918
|
-
// ../canopycms/src/
|
|
48900
|
+
// ../canopycms/src/task-queue/worker-status.ts
|
|
48919
48901
|
import path4 from "node:path";
|
|
48920
48902
|
var WORKER_STATUS_FILE = "worker-status.json";
|
|
48921
48903
|
async function writeWorkerStatus(taskDir, report) {
|
|
@@ -49068,15 +49050,12 @@ var BranchRegistry = class {
|
|
|
49068
49050
|
return await this.regenerate();
|
|
49069
49051
|
}
|
|
49070
49052
|
/**
|
|
49071
|
-
* Returns a single branch by name
|
|
49053
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
49072
49054
|
*
|
|
49073
|
-
* Suspicious-miss backstop:
|
|
49074
|
-
*
|
|
49075
|
-
*
|
|
49076
|
-
*
|
|
49077
|
-
* genuinely-absent branch costs at most one extra scan per throttle window.
|
|
49078
|
-
* This bounds durable window-E staleness for the "branch exists but
|
|
49079
|
-
* snapshot predates it" direction; see the class doc comment.
|
|
49055
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
49056
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
49057
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
49058
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
49080
49059
|
*/
|
|
49081
49060
|
async get(name) {
|
|
49082
49061
|
const branches = await this.list();
|
|
@@ -49093,14 +49072,11 @@ var BranchRegistry = class {
|
|
|
49093
49072
|
return true;
|
|
49094
49073
|
}
|
|
49095
49074
|
/**
|
|
49096
|
-
* Marks the cache
|
|
49097
|
-
*
|
|
49098
|
-
*
|
|
49099
|
-
*
|
|
49100
|
-
*
|
|
49101
|
-
* closing window (E) early - the bump alone already restored correctness
|
|
49102
|
-
* for every future reader. A failed eager regen therefore must not fail the
|
|
49103
|
-
* caller's save/delete.
|
|
49075
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
49076
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
49077
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
49078
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
49079
|
+
* bump alone already restored correctness for every future reader.
|
|
49104
49080
|
*/
|
|
49105
49081
|
async invalidate() {
|
|
49106
49082
|
await bumpResourceGeneration(this.root, RESOURCE, { mustSucceed: true });
|
|
@@ -49117,9 +49093,8 @@ var BranchRegistry = class {
|
|
|
49117
49093
|
/**
|
|
49118
49094
|
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
49119
49095
|
* callers on this instance to a single underlying scan. Never loops waiting
|
|
49120
|
-
* for the
|
|
49121
|
-
* storm
|
|
49122
|
-
* after a concurrent bump should call list() again after this resolves.
|
|
49096
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
49097
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
49123
49098
|
*/
|
|
49124
49099
|
async regenerate() {
|
|
49125
49100
|
if (this.regenInFlight) return this.regenInFlight;
|
|
@@ -49371,16 +49346,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
49371
49346
|
this.baseRoot = baseRoot;
|
|
49372
49347
|
this.settleMs = options?.settleMs;
|
|
49373
49348
|
}
|
|
49374
|
-
/**
|
|
49375
|
-
* Load branch metadata without requiring baseRoot.
|
|
49376
|
-
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
49377
|
-
*/
|
|
49349
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
49378
49350
|
static async loadOnly(branchRoot) {
|
|
49379
49351
|
return readBranchMetadataFile(branchRoot);
|
|
49380
49352
|
}
|
|
49381
49353
|
/**
|
|
49382
|
-
*
|
|
49383
|
-
*
|
|
49354
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
49355
|
+
* registry cache is invalidated on update.
|
|
49384
49356
|
*/
|
|
49385
49357
|
static get(branchRoot, baseRoot, options) {
|
|
49386
49358
|
return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
|
|
@@ -49399,18 +49371,16 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
49399
49371
|
}
|
|
49400
49372
|
}
|
|
49401
49373
|
/**
|
|
49402
|
-
* Write branch.json via the shared OCC helper,
|
|
49403
|
-
* default
|
|
49374
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
49375
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
49404
49376
|
*
|
|
49405
|
-
* Throws the helper's raw {@link OccWriteConflictError}
|
|
49406
|
-
* {@link withOccRetry} in save() recognizes and retries
|
|
49407
|
-
* the public `BranchMetadataConflictError`
|
|
49408
|
-
* boundary
|
|
49409
|
-
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
49410
|
-
* raw error type).
|
|
49377
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
49378
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
49379
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
49380
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
49411
49381
|
*
|
|
49412
|
-
* branch
|
|
49413
|
-
*
|
|
49382
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
49383
|
+
* unlike comments.json.
|
|
49414
49384
|
*/
|
|
49415
49385
|
async write(meta, expectedVersion) {
|
|
49416
49386
|
const payload2 = {
|
|
@@ -49424,28 +49394,22 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
49424
49394
|
});
|
|
49425
49395
|
}
|
|
49426
49396
|
/**
|
|
49427
|
-
* Run a save cycle under the full lock + OCC-retry stack
|
|
49428
|
-
*
|
|
49429
|
-
* public `BranchMetadataConflictError`.
|
|
49397
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
49398
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
49430
49399
|
*
|
|
49431
|
-
*
|
|
49432
|
-
* caller's branchContext can
|
|
49433
|
-
*
|
|
49434
|
-
*
|
|
49435
|
-
*
|
|
49436
|
-
*
|
|
49437
|
-
*
|
|
49438
|
-
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
49439
|
-
* (rather than after) fails fast without paying for a lock acquisition on
|
|
49440
|
-
* a doomed save.
|
|
49400
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
49401
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
49402
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
49403
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
49404
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
49405
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
49406
|
+
* save fails fast instead of paying for a lock.
|
|
49441
49407
|
*
|
|
49442
|
-
*
|
|
49443
|
-
*
|
|
49444
|
-
*
|
|
49445
|
-
*
|
|
49446
|
-
*
|
|
49447
|
-
* it cannot itself provide a wider guarantee than "the directory existed
|
|
49448
|
-
* a moment ago."
|
|
49408
|
+
* Accepted residual window: a save that passes the check can still race a
|
|
49409
|
+
* `rm` that starts moments later and is mid-flight when the write lands.
|
|
49410
|
+
* Closing that needs a tombstone OUTSIDE the tree being removed, and the
|
|
49411
|
+
* lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
|
|
49412
|
+
* than "the directory existed a moment ago".
|
|
49449
49413
|
*/
|
|
49450
49414
|
async save(incoming) {
|
|
49451
49415
|
try {
|
|
@@ -49489,9 +49453,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
49489
49453
|
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
49490
49454
|
// Fork point is recorded once at creation; later saves must not move it
|
|
49491
49455
|
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
49492
|
-
// Always stamped fresh
|
|
49493
|
-
// the
|
|
49494
|
-
//
|
|
49456
|
+
// Always stamped fresh; the spreads above would otherwise let
|
|
49457
|
+
// the creation-time value win forever, freezing the timestamp
|
|
49458
|
+
// the editor's Branches panel sorts and displays by
|
|
49495
49459
|
updatedAt: now
|
|
49496
49460
|
}
|
|
49497
49461
|
};
|
|
@@ -49511,9 +49475,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
49511
49475
|
await this.invalidateRegistry();
|
|
49512
49476
|
return saved;
|
|
49513
49477
|
}
|
|
49514
|
-
/**
|
|
49515
|
-
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
49516
|
-
*/
|
|
49478
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
49517
49479
|
async invalidateRegistry() {
|
|
49518
49480
|
const registry2 = new BranchRegistry(this.baseRoot);
|
|
49519
49481
|
await registry2.invalidate();
|
|
@@ -49695,12 +49657,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
|
|
|
49695
49657
|
realpath: false,
|
|
49696
49658
|
retries,
|
|
49697
49659
|
stale: 3e4,
|
|
49698
|
-
// proper-lockfile invokes this from inside its refresh timer, so ANY throw
|
|
49699
|
-
//
|
|
49700
|
-
//
|
|
49701
|
-
//
|
|
49702
|
-
//
|
|
49703
|
-
// vitest's `onConsoleLog` turns any console write into a throw.
|
|
49660
|
+
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
49661
|
+
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
49662
|
+
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
49663
|
+
// LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
|
|
49664
|
+
// throw.
|
|
49704
49665
|
onCompromised: (err) => {
|
|
49705
49666
|
try {
|
|
49706
49667
|
if (onCompromised) {
|
|
@@ -50104,11 +50065,9 @@ async function updateBranchMetadata(ctx, task, result) {
|
|
|
50104
50065
|
const updates = {
|
|
50105
50066
|
name: branch,
|
|
50106
50067
|
syncStatus: "synced",
|
|
50107
|
-
// save()'s merge only overwrites keys present in this update
|
|
50108
|
-
//
|
|
50109
|
-
//
|
|
50110
|
-
// survive forever past this successful sync. Explicitly clear it,
|
|
50111
|
-
// same pattern as rebaseFailure's PR-W2 M2 clearing elsewhere.
|
|
50068
|
+
// save()'s merge only overwrites keys present in this update, so a prior
|
|
50069
|
+
// syncFailureReason would survive this successful sync forever unless
|
|
50070
|
+
// cleared explicitly (same pattern as rebaseFailure elsewhere).
|
|
50112
50071
|
syncFailureReason: void 0
|
|
50113
50072
|
};
|
|
50114
50073
|
if (result.prUrl) updates.pullRequestUrl = result.prUrl;
|
|
@@ -50152,9 +50111,9 @@ async function updateBranchMetadataOnFailure(ctx, task, error2) {
|
|
|
50152
50111
|
async function pushBranchToGitHub(ctx, branch) {
|
|
50153
50112
|
const git = simpleGit({
|
|
50154
50113
|
baseDir: ctx.remoteGitPath,
|
|
50155
|
-
// DEP-H1: kill the git process if it produces no output for
|
|
50156
|
-
//
|
|
50157
|
-
//
|
|
50114
|
+
// DEP-H1: kill the git process if it produces no output for taskTimeoutMs
|
|
50115
|
+
// (network stall, credential prompt) instead of letting it hang past the
|
|
50116
|
+
// task timeout.
|
|
50158
50117
|
timeout: { block: ctx.taskTimeoutMs }
|
|
50159
50118
|
});
|
|
50160
50119
|
git.env(gitNetworkChildEnv());
|
|
@@ -50441,10 +50400,10 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
50441
50400
|
}
|
|
50442
50401
|
const branchGit = simpleGit({
|
|
50443
50402
|
baseDir: branchPath,
|
|
50444
|
-
// Keep git non-interactive during rebase/merge so it never blocks on an
|
|
50445
|
-
// simple-git >=3.32
|
|
50446
|
-
// value
|
|
50447
|
-
// so
|
|
50403
|
+
// Keep git non-interactive during rebase/merge so it never blocks on an
|
|
50404
|
+
// editor. simple-git >=3.32 requires opting in to set core.editor; the
|
|
50405
|
+
// value is a hardcoded literal ("true", the shell no-op), not user input,
|
|
50406
|
+
// so allowUnsafeEditor carries no injection risk here.
|
|
50448
50407
|
config: ["core.editor=true"],
|
|
50449
50408
|
unsafe: { allowUnsafeEditor: true }
|
|
50450
50409
|
});
|
|
@@ -50575,8 +50534,8 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
50575
50534
|
name: branchDir,
|
|
50576
50535
|
conflictStatus: hadConflicts ? "conflicts-detected" : "clean",
|
|
50577
50536
|
conflictFiles: conflictIdsDeduped,
|
|
50578
|
-
//
|
|
50579
|
-
//
|
|
50537
|
+
// The cycle completed, so clear any prior failure record regardless
|
|
50538
|
+
// of the conflict outcome.
|
|
50580
50539
|
rebaseFailure: void 0
|
|
50581
50540
|
}
|
|
50582
50541
|
});
|
|
@@ -50873,18 +50832,14 @@ async function refreshBaseBranchWorkspace(ctx) {
|
|
|
50873
50832
|
const baseGit = simpleGit({
|
|
50874
50833
|
baseDir: basePath37,
|
|
50875
50834
|
// Keep git non-interactive during the merge so it never blocks on an
|
|
50876
|
-
// editor. simple-git >=3.32
|
|
50877
|
-
//
|
|
50878
|
-
//
|
|
50879
|
-
// allowUnsafeEditor carries no injection risk (same as the rebase
|
|
50880
|
-
// loop's git config below).
|
|
50835
|
+
// editor. simple-git >=3.32 requires opting in to set core.editor; the
|
|
50836
|
+
// value is a hardcoded literal ("true", the shell no-op), not user input,
|
|
50837
|
+
// so allowUnsafeEditor carries no injection risk here.
|
|
50881
50838
|
config: ["core.editor=true"],
|
|
50882
50839
|
unsafe: { allowUnsafeEditor: true },
|
|
50883
|
-
// DEP-H1: a hung fetch/merge against this EFS-backed clone would
|
|
50884
|
-
//
|
|
50885
|
-
//
|
|
50886
|
-
// slow-but-flowing transfer is unaffected -- same as syncGit()'s
|
|
50887
|
-
// remote handle.
|
|
50840
|
+
// DEP-H1: a hung fetch/merge against this EFS-backed clone would stall
|
|
50841
|
+
// the sync loop forever (scheduleLoop only reschedules after completion).
|
|
50842
|
+
// Inactivity-based, so a slow-but-flowing transfer is unaffected.
|
|
50888
50843
|
timeout: { block: ctx.taskTimeoutMs }
|
|
50889
50844
|
});
|
|
50890
50845
|
const status = await baseGit.status();
|
|
@@ -50957,12 +50912,10 @@ var CmsWorker = class {
|
|
|
50957
50912
|
this.contentRoot = config.contentRoot ?? "content";
|
|
50958
50913
|
}
|
|
50959
50914
|
/**
|
|
50960
|
-
*
|
|
50961
|
-
*
|
|
50962
|
-
* start()
|
|
50963
|
-
*
|
|
50964
|
-
* (defensive -- see start()'s catch), and (2) unit tests that exercise
|
|
50965
|
-
* syncGit()/processTaskQueue() directly without calling start() first.
|
|
50915
|
+
* This worker's self-reported status object, initialized on first use.
|
|
50916
|
+
* Normally set at the top of start(); the lazy fallback covers something in
|
|
50917
|
+
* start() reaching a status-write point before that (see start()'s catch) and
|
|
50918
|
+
* unit tests driving syncGit()/processTaskQueue() without calling start().
|
|
50966
50919
|
*/
|
|
50967
50920
|
ensureStatusReport() {
|
|
50968
50921
|
if (!this.statusReport) {
|
|
@@ -50975,28 +50928,23 @@ var CmsWorker = class {
|
|
|
50975
50928
|
* Resolve this deployment's settings branch, throwing if the infra-stamped
|
|
50976
50929
|
* deployment name is not a valid git ref component.
|
|
50977
50930
|
*
|
|
50978
|
-
* Routed through the shared resolver, not a local `?? 'prod'`:
|
|
50931
|
+
* Routed through the shared resolver, not a local `?? 'prod'`: that is the
|
|
50979
50932
|
* single definition of the env > config > mode-default precedence the Lambda
|
|
50980
|
-
*
|
|
50981
|
-
*
|
|
50982
|
-
*
|
|
50983
|
-
*
|
|
50933
|
+
* follows, and the only place the resolved value is validated. Without it the
|
|
50934
|
+
* worker can silently own a different settings branch than the Lambda writing
|
|
50935
|
+
* to the same workspace, and pushSettingsBranches then reports the real branch
|
|
50936
|
+
* as foreign and never pushes it.
|
|
50984
50937
|
*
|
|
50985
|
-
* DEFERRED out of the constructor deliberately,
|
|
50986
|
-
*
|
|
50987
|
-
* CmsWorker(...)`
|
|
50988
|
-
*
|
|
50989
|
-
*
|
|
50990
|
-
*
|
|
50991
|
-
*
|
|
50992
|
-
* crash-loop that `cdk deploy` reported as success while the admin panel
|
|
50993
|
-
* showed the worker as 'absent' with no fatal error to explain it. Resolving
|
|
50994
|
-
* inside start()'s try block instead means the same throw is recorded to
|
|
50995
|
-
* worker-status.json and surfaces in the admin panel.
|
|
50938
|
+
* DEFERRED out of the constructor deliberately, which is the whole point of
|
|
50939
|
+
* the method existing: canopycms-cdk/worker/index.ts constructs the worker
|
|
50940
|
+
* before calling start(), so a throw during `new CmsWorker(...)` lands BEFORE
|
|
50941
|
+
* the only code that writes `lastFatalError` -- start()'s catch. Under
|
|
50942
|
+
* systemd `Type=simple` + `Restart=always` with no cfn-signal, that is an
|
|
50943
|
+
* invisible ~5s crash-loop that `cdk deploy` reports as success while the
|
|
50944
|
+
* admin panel shows the worker 'absent' with no fatal error to explain it.
|
|
50996
50945
|
*
|
|
50997
|
-
* Lazy rather than start()-only so
|
|
50998
|
-
*
|
|
50999
|
-
* same reason ensureStatusReport() above is shaped this way. Idempotent: the
|
|
50946
|
+
* Lazy rather than start()-only so unit tests driving pushSettingsBranches()
|
|
50947
|
+
* still see the value, exactly as ensureStatusReport() above. Idempotent: the
|
|
51000
50948
|
* resolver is pure, so a later call returns the identical string.
|
|
51001
50949
|
*/
|
|
51002
50950
|
ensureSettingsBranch() {
|
|
@@ -51009,15 +50957,11 @@ var CmsWorker = class {
|
|
|
51009
50957
|
* Build the {@link WorkerContext} handed to the extracted clusters
|
|
51010
50958
|
* (task-runner.ts, git-sync.ts, rebase.ts, history-rewrite.ts).
|
|
51011
50959
|
*
|
|
51012
|
-
* Built FRESH on every call
|
|
51013
|
-
*
|
|
51014
|
-
*
|
|
51015
|
-
*
|
|
51016
|
-
*
|
|
51017
|
-
* hooks. A context that captured any of those at construction time would hand
|
|
51018
|
-
* the extracted code the pre-test value -- which for `buildGitHubUrl` means a
|
|
51019
|
-
* test's push going to github.com for real instead of its local fixture repo.
|
|
51020
|
-
* See WorkerContext's doc comment for the full list.
|
|
50960
|
+
* Built FRESH on every call, with instance-backed members as functions rather
|
|
50961
|
+
* than copied values. See WorkerContext's INVARIANT: a context that captured
|
|
50962
|
+
* any of them at construction time would hand the extracted code the pre-test
|
|
50963
|
+
* value, which for `buildGitHubUrl` means a test's push going to github.com
|
|
50964
|
+
* for real instead of its local fixture repo.
|
|
51021
50965
|
*/
|
|
51022
50966
|
ctx() {
|
|
51023
50967
|
return {
|
|
@@ -51072,9 +51016,9 @@ var CmsWorker = class {
|
|
|
51072
51016
|
} catch (err) {
|
|
51073
51017
|
const report = this.ensureStatusReport();
|
|
51074
51018
|
report.lastFatalError = {
|
|
51075
|
-
// [REDACT] Persisted to worker-status.json and served to the
|
|
51076
|
-
//
|
|
51077
|
-
//
|
|
51019
|
+
// [REDACT] Persisted to worker-status.json and served to the browser by
|
|
51020
|
+
// the admin panel -- must never carry the bot token a poisoned or
|
|
51021
|
+
// failed git URL (buildGitHubUrl()) can embed.
|
|
51078
51022
|
message: redactCredentials(getErrorMessage2(err)),
|
|
51079
51023
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
51080
51024
|
phase: "startup"
|
|
@@ -51124,26 +51068,23 @@ var CmsWorker = class {
|
|
|
51124
51068
|
* Acquire the cross-host worker lock (DEP-C2).
|
|
51125
51069
|
*
|
|
51126
51070
|
* The task queue is single-consumer (see task-queue/task-queue.ts): two
|
|
51127
|
-
* concurrent workers would double-process tasks
|
|
51128
|
-
*
|
|
51129
|
-
*
|
|
51130
|
-
*
|
|
51131
|
-
* and must never participate in staleness decisions.
|
|
51071
|
+
* concurrent workers would double-process tasks, duplicating pushes and PRs.
|
|
51072
|
+
* The workspace lives on EFS, so mutual exclusion must be sound ACROSS HOSTS
|
|
51073
|
+
* — a PID liveness probe (`process.kill(pid, 0)`) means something only on the
|
|
51074
|
+
* holder's own machine and must NEVER participate in staleness decisions.
|
|
51132
51075
|
*
|
|
51133
51076
|
* proper-lockfile provides a heartbeat lease with no PID involved: the lock
|
|
51134
51077
|
* is a directory created atomically (mkdir — atomic on NFS/EFS), the holder
|
|
51135
|
-
* refreshes its mtime every lockStaleMs/2, and the lock is
|
|
51136
|
-
*
|
|
51137
|
-
* lockStaleMs. Liveness is judged purely by heartbeat freshness.
|
|
51078
|
+
* refreshes its mtime every lockStaleMs/2, and the lock is abandoned, and
|
|
51079
|
+
* taken over, only once that heartbeat is older than lockStaleMs.
|
|
51138
51080
|
*
|
|
51139
51081
|
* No acquire retries: a second worker exits immediately, matching daemon
|
|
51140
|
-
* semantics
|
|
51141
|
-
*
|
|
51082
|
+
* semantics. After a crash the dead holder's heartbeat expires within
|
|
51083
|
+
* lockStaleMs and the next start succeeds.
|
|
51142
51084
|
*
|
|
51143
|
-
* Staleness
|
|
51144
|
-
*
|
|
51145
|
-
*
|
|
51146
|
-
* negligible, but a host with a badly wrong clock could misjudge liveness.
|
|
51085
|
+
* Staleness compares the lock's mtime against the LOCAL clock, so correct
|
|
51086
|
+
* cross-host takeover assumes reasonable clock agreement (NTP). Ordinary skew
|
|
51087
|
+
* is negligible at the default TTL; a badly wrong clock misjudges liveness.
|
|
51147
51088
|
*/
|
|
51148
51089
|
async acquireLock() {
|
|
51149
51090
|
await fs13.mkdir(this.taskDir, { recursive: true });
|
|
@@ -51176,9 +51117,9 @@ var CmsWorker = class {
|
|
|
51176
51117
|
}
|
|
51177
51118
|
}
|
|
51178
51119
|
/**
|
|
51179
|
-
*
|
|
51180
|
-
*
|
|
51181
|
-
*
|
|
51120
|
+
* Run `fn` repeatedly, the next invocation starting `interval` ms after the
|
|
51121
|
+
* previous one COMPLETES. setTimeout chaining rather than setInterval, so
|
|
51122
|
+
* executions cannot overlap when one runs longer than the interval.
|
|
51182
51123
|
*/
|
|
51183
51124
|
scheduleLoop(fn, interval) {
|
|
51184
51125
|
const run = () => {
|
|
@@ -51200,18 +51141,16 @@ var CmsWorker = class {
|
|
|
51200
51141
|
/**
|
|
51201
51142
|
* Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
|
|
51202
51143
|
*
|
|
51203
|
-
*
|
|
51204
|
-
*
|
|
51205
|
-
*
|
|
51206
|
-
*
|
|
51207
|
-
* GitManager.bareRemoteHasBranch for the same pattern).
|
|
51144
|
+
* Explicit `--git-dir` rather than `simpleGit({ baseDir })`, so this also
|
|
51145
|
+
* works where `safe.bareRepository=explicit` refuses cwd-based discovery of
|
|
51146
|
+
* bare repos but expressly allows `--git-dir` (same pattern as
|
|
51147
|
+
* GitManager.bareRemoteHasBranch).
|
|
51208
51148
|
*
|
|
51209
|
-
* Deliberately omits `--quiet`: simple-git
|
|
51210
|
-
*
|
|
51211
|
-
*
|
|
51212
|
-
*
|
|
51213
|
-
*
|
|
51214
|
-
* is what makes simple-git reject the promise here.
|
|
51149
|
+
* Deliberately omits `--quiet`: simple-git treats a task as failed only when
|
|
51150
|
+
* the process exits non-zero AND writes to stderr, so a silent-on-failure
|
|
51151
|
+
* `--verify` would leave a missing branch indistinguishable from success.
|
|
51152
|
+
* Without `--quiet`, `rev-parse --verify` writes "fatal: ..." to stderr,
|
|
51153
|
+
* which is what makes simple-git reject the promise here.
|
|
51215
51154
|
*/
|
|
51216
51155
|
async verifyBaseBranchExists(gitDir) {
|
|
51217
51156
|
await simpleGit().raw([
|
|
@@ -51228,20 +51167,17 @@ var CmsWorker = class {
|
|
|
51228
51167
|
*
|
|
51229
51168
|
* `git clone https://x-access-token:<token>@github.com/...` records that URL
|
|
51230
51169
|
* verbatim as `remote.origin.url`, and for `remote.git` that config lives on
|
|
51231
|
-
* shared EFS. The security model in docs/deploying-to-aws.md --
|
|
51232
|
-
*
|
|
51233
|
-
*
|
|
51234
|
-
*
|
|
51235
|
-
*
|
|
51236
|
-
* content the worker then pushes to GitHub.
|
|
51170
|
+
* shared EFS. The security model in docs/deploying-to-aws.md -- a compromised
|
|
51171
|
+
* Lambda can read/write EFS content but cannot push to GitHub, secrets stay
|
|
51172
|
+
* on the worker -- is false while that string is there: the Lambda could read
|
|
51173
|
+
* the token off EFS and, with no egress of its own, exfiltrate it by writing
|
|
51174
|
+
* it into branch content the worker then pushes to GitHub.
|
|
51237
51175
|
*
|
|
51238
51176
|
* Nothing needs the remote: every push passes the URL explicitly as an
|
|
51239
|
-
* argument
|
|
51240
|
-
* `verifyBaseBranchExists` reads local refs.
|
|
51177
|
+
* argument, and `verifyBaseBranchExists` reads local refs.
|
|
51241
51178
|
*
|
|
51242
51179
|
* VERIFIES rather than assuming: it re-reads the config and throws if the URL
|
|
51243
|
-
* survives,
|
|
51244
|
-
* scrub was indistinguishable from a successful one.
|
|
51180
|
+
* survives, so a failed scrub is never indistinguishable from a clean one.
|
|
51245
51181
|
*/
|
|
51246
51182
|
async scrubPersistedRemote(gitDir) {
|
|
51247
51183
|
const git = simpleGit({ baseDir: gitDir });
|
|
@@ -51275,19 +51211,17 @@ var CmsWorker = class {
|
|
|
51275
51211
|
}
|
|
51276
51212
|
}
|
|
51277
51213
|
/**
|
|
51278
|
-
* Ensure remote.git bare repo exists
|
|
51279
|
-
*
|
|
51214
|
+
* Ensure the remote.git bare repo exists, cloning it from GitHub on first
|
|
51215
|
+
* run.
|
|
51280
51216
|
*
|
|
51281
51217
|
* Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
|
|
51282
|
-
* commits, or a base branch
|
|
51283
|
-
*
|
|
51284
|
-
*
|
|
51285
|
-
*
|
|
51286
|
-
*
|
|
51287
|
-
*
|
|
51288
|
-
*
|
|
51289
|
-
* previous run could have left a poisoned remote.git behind before this
|
|
51290
|
-
* guard existed.
|
|
51218
|
+
* commits, or a base branch never pushed) exits 0 and produces a refs-less
|
|
51219
|
+
* bare repo whose HEAD points at an unborn branch. `fs.stat` cannot tell that
|
|
51220
|
+
* from a healthy clone, so left unchecked it silently poisons remote.git —
|
|
51221
|
+
* every later branch operation breaks and the fs.stat short-circuit means it
|
|
51222
|
+
* never heals. The base branch is therefore verified right after cloning AND
|
|
51223
|
+
* on the already-exists fast path, since a previous run can have left a
|
|
51224
|
+
* poisoned remote.git behind.
|
|
51291
51225
|
*/
|
|
51292
51226
|
async ensureRemoteGit() {
|
|
51293
51227
|
let exists2;
|
|
@@ -51329,24 +51263,18 @@ var CmsWorker = class {
|
|
|
51329
51263
|
}
|
|
51330
51264
|
// --- Task-queue cluster (worker/task-runner.ts) ------------------------
|
|
51331
51265
|
//
|
|
51332
|
-
// READ THIS BEFORE STUBBING ANY DELEGATOR BELOW
|
|
51333
|
-
// and the difference is invisible from here.
|
|
51266
|
+
// READ THIS BEFORE STUBBING ANY DELEGATOR BELOW: they are not all the same,
|
|
51267
|
+
// and the difference is invisible from here. `processTaskQueue` is the public
|
|
51268
|
+
// loop entry `scheduleLoop` drives, so it IS on the production path; the
|
|
51269
|
+
// other three exist ONLY so test files can reach the implementations through
|
|
51270
|
+
// the instance, since the extracted modules call each other at module level.
|
|
51334
51271
|
//
|
|
51335
|
-
//
|
|
51336
|
-
//
|
|
51337
|
-
//
|
|
51338
|
-
//
|
|
51339
|
-
//
|
|
51340
|
-
//
|
|
51341
|
-
// The consequence: replacing one of these on an instance only affects
|
|
51342
|
-
// production behaviour if the context also routes it. Two do --
|
|
51343
|
-
// `executeTask` and `pushBranchToGitHub` are on `WorkerContext` precisely
|
|
51344
|
-
// because cms-worker.test.ts assigns over them and then asserts on the
|
|
51345
|
-
// replacement. `updateBranchMetadata` is NOT: the test calls it directly and
|
|
51346
|
-
// never stubs it, so a stub installed there today would be a silent no-op.
|
|
51347
|
-
// If you need to stub a method that isn't on the context, add it to
|
|
51348
|
-
// WorkerContext and route the internal caller through `ctx` -- do not assume
|
|
51349
|
-
// the delegator's existence means the stub takes effect.
|
|
51272
|
+
// So replacing one of these on an instance affects production behaviour only
|
|
51273
|
+
// if the context also routes it. `executeTask` and `pushBranchToGitHub` are
|
|
51274
|
+
// on `WorkerContext` for exactly that reason; `updateBranchMetadata` is NOT,
|
|
51275
|
+
// so a stub installed there is a silent no-op. To stub a method that is not
|
|
51276
|
+
// on the context, add it to WorkerContext and route the internal caller
|
|
51277
|
+
// through `ctx` -- a delegator's existence does not make a stub take effect.
|
|
51350
51278
|
async processTaskQueue() {
|
|
51351
51279
|
return processTaskQueue(this.ctx());
|
|
51352
51280
|
}
|
|
@@ -51364,16 +51292,11 @@ var CmsWorker = class {
|
|
|
51364
51292
|
* Octokit client from it.
|
|
51365
51293
|
*
|
|
51366
51294
|
* DEFERRED out of the constructor deliberately, exactly as
|
|
51367
|
-
* `ensureSettingsBranch()` is
|
|
51295
|
+
* `ensureSettingsBranch()` is and for the reason that method records:
|
|
51368
51296
|
* `resolveWorkerGitHubAuth` throws for a half-configured credential (both
|
|
51369
|
-
* set, neither set, an unusable mint timeout or refresh interval), and a
|
|
51370
|
-
* `new CmsWorker(...)` lands BEFORE the only code that writes
|
|
51371
|
-
* `lastFatalError` — start()'s catch.
|
|
51372
|
-
* worker and calls start() separately, and its `main().catch()` only logs
|
|
51373
|
-
* and exits, so a constructor throw is an invisible ~5s systemd crash-loop
|
|
51374
|
-
* that `cdk deploy` reports as success while the admin panel shows the
|
|
51375
|
-
* worker 'absent' with no fatal error to explain it. That is a shipped
|
|
51376
|
-
* regression this codebase has already paid for once (#198).
|
|
51297
|
+
* set, neither set, an unusable mint timeout or refresh interval), and a
|
|
51298
|
+
* throw during `new CmsWorker(...)` lands BEFORE the only code that writes
|
|
51299
|
+
* `lastFatalError` — start()'s catch.
|
|
51377
51300
|
*
|
|
51378
51301
|
* Idempotent, and it does NOT replace an `octokit` a test has already
|
|
51379
51302
|
* assigned onto the instance — see the field's comment.
|
|
@@ -51388,34 +51311,29 @@ var CmsWorker = class {
|
|
|
51388
51311
|
return this.githubAuth;
|
|
51389
51312
|
}
|
|
51390
51313
|
/**
|
|
51391
|
-
* The Octokit client, built on first use.
|
|
51392
|
-
*
|
|
51393
|
-
*
|
|
51394
|
-
*
|
|
51395
|
-
*
|
|
51396
|
-
* exactly that case — apps/test-app's e2e route calls it directly, and its
|
|
51397
|
-
* `pollMergeState` dispatch reads `ctx.octokit()`.
|
|
51314
|
+
* The Octokit client, built on first use. Every read goes through here rather
|
|
51315
|
+
* than touching the field, which the constructor does not populate: a method
|
|
51316
|
+
* reached without start() would otherwise see `undefined`.
|
|
51317
|
+
* `rebaseActiveBranches()` is exactly that case — apps/test-app's e2e route
|
|
51318
|
+
* calls it directly, and its `pollMergeState` dispatch reads `ctx.octokit()`.
|
|
51398
51319
|
*/
|
|
51399
51320
|
octokitClient() {
|
|
51400
51321
|
this.ensureGitHubAuth();
|
|
51401
51322
|
return this.octokit;
|
|
51402
51323
|
}
|
|
51403
51324
|
/**
|
|
51404
|
-
* The single seam through which every git-over-HTTPS credential reaches a
|
|
51405
|
-
*
|
|
51406
|
-
*
|
|
51325
|
+
* The single seam through which every git-over-HTTPS credential reaches a git
|
|
51326
|
+
* command. The only other consumer is Octokit, built from the same resolution
|
|
51327
|
+
* by `ensureGitHubAuth()` above.
|
|
51407
51328
|
*
|
|
51408
|
-
* Async because
|
|
51409
|
-
*
|
|
51410
|
-
*
|
|
51411
|
-
*
|
|
51412
|
-
*
|
|
51413
|
-
* token is near expiry, so the usual cost is a resolved microtask, and the
|
|
51414
|
-
* token path is a bare `async` return.
|
|
51329
|
+
* Async because under GitHub App auth `resolveGitToken` mints an installation
|
|
51330
|
+
* token lasting about an hour. NOTHING may cache what this returns — a URL
|
|
51331
|
+
* built from an installation token goes stale with it — and resolving per use
|
|
51332
|
+
* is cheap, since `@octokit/auth-app` answers from its own cache until the
|
|
51333
|
+
* token is near expiry.
|
|
51415
51334
|
*
|
|
51416
51335
|
* A mint failure propagates AS THROWN, carrying the `.status` that
|
|
51417
|
-
* `isPermanentTaskFailure`
|
|
51418
|
-
* github-auth.ts.
|
|
51336
|
+
* `isPermanentTaskFailure` classifies on — see github-auth.ts.
|
|
51419
51337
|
*
|
|
51420
51338
|
* Do NOT add a parallel token accessor alongside it. Every instance-backed
|
|
51421
51339
|
* WorkerContext member stays a function precisely so tests can replace it
|
|
@@ -51429,36 +51347,31 @@ var CmsWorker = class {
|
|
|
51429
51347
|
/**
|
|
51430
51348
|
* Prove the GitHub App credential works before anything depends on it.
|
|
51431
51349
|
*
|
|
51432
|
-
* Without this the first failure comes out of `ensureRemoteGit`'s bare
|
|
51433
|
-
*
|
|
51434
|
-
*
|
|
51435
|
-
*
|
|
51436
|
-
*
|
|
51437
|
-
*
|
|
51350
|
+
* Without this the first failure comes out of `ensureRemoteGit`'s bare clone
|
|
51351
|
+
* below, whose catch blames the repository ("may be empty, or the base branch
|
|
51352
|
+
* may not exist") and sends an operator holding a bad private key looking for
|
|
51353
|
+
* a problem that does not exist. Called from start()'s try, so the failure is
|
|
51354
|
+
* also recorded as `lastFatalError` and reaches the admin panel. No-op on the
|
|
51355
|
+
* token path: a PAT is a literal, so the first real request checks everything
|
|
51356
|
+
* this could.
|
|
51438
51357
|
*
|
|
51439
|
-
*
|
|
51440
|
-
*
|
|
51441
|
-
*
|
|
51442
|
-
*
|
|
51443
|
-
*
|
|
51444
|
-
*
|
|
51445
|
-
*
|
|
51446
|
-
*
|
|
51447
|
-
* short-circuits `ensureRemoteGit`, and `Promise.allSettled` swallows the
|
|
51448
|
-
* initial `syncGit`), so the worker starts and its loops retry. Rethrowing
|
|
51449
|
-
* every error class would make the App path exit instead, and systemd
|
|
51450
|
-
* (Restart=always) would crash-loop it until GitHub recovered — each
|
|
51451
|
-
* iteration telling the operator to go and check their private key.
|
|
51358
|
+
* FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves are
|
|
51359
|
+
* load-bearing. Not always fatal, because the two credential paths must
|
|
51360
|
+
* degrade alike: on the token path a GitHub 502 during boot is absorbed (a
|
|
51361
|
+
* warm `remote.git` short-circuits `ensureRemoteGit`, `Promise.allSettled`
|
|
51362
|
+
* swallows the initial `syncGit`) so the worker starts and its loops retry,
|
|
51363
|
+
* while rethrowing every error class would make the App path exit and systemd
|
|
51364
|
+
* crash-loop it until GitHub recovered — each iteration telling the operator
|
|
51365
|
+
* to check their private key.
|
|
51452
51366
|
*
|
|
51453
51367
|
* But fail CLOSED, via `isTransientAuthFailure` rather than the inverse of
|
|
51454
|
-
* `isPermanentTaskFailure
|
|
51455
|
-
*
|
|
51456
|
-
*
|
|
51457
|
-
*
|
|
51458
|
-
*
|
|
51459
|
-
*
|
|
51460
|
-
*
|
|
51461
|
-
* isTransientAuthFailure in github-auth.ts.
|
|
51368
|
+
* `isPermanentTaskFailure`: that classifier defaults a status-less error to
|
|
51369
|
+
* transient, which is right on the task path (bounded by `maxRetries`) and
|
|
51370
|
+
* wrong here (bounded by nothing). A key that never reaches GitHub at all —
|
|
51371
|
+
* the wrong type, or too mangled to sign with — fails locally and
|
|
51372
|
+
* status-lessly, so defaulting to transient would boot a worker with a dead
|
|
51373
|
+
* credential, record no `lastFatalError`, and show healthy in the admin panel
|
|
51374
|
+
* while every task and every sync failed.
|
|
51462
51375
|
*/
|
|
51463
51376
|
async preflightGitHubAppAuth() {
|
|
51464
51377
|
if (!this.config.githubAppAuth) return;
|
|
@@ -51479,20 +51392,19 @@ var CmsWorker = class {
|
|
|
51479
51392
|
workerLog("GitHub App authentication verified");
|
|
51480
51393
|
}
|
|
51481
51394
|
/**
|
|
51482
|
-
* The workspace directory for a branch named by its GIT REF name -- the
|
|
51483
|
-
*
|
|
51395
|
+
* The workspace directory for a branch named by its GIT REF name -- the form
|
|
51396
|
+
* task payloads carry (`context.branch.name`), not the directory form.
|
|
51484
51397
|
*
|
|
51485
51398
|
* These differ for any name outside `[A-Za-z0-9._-]`, `/` being the obvious
|
|
51486
51399
|
* one: workspaces are provisioned under `sanitizeBranchName(...)` (see
|
|
51487
51400
|
* paths/branch.ts's `resolveBranchPaths`), so `feature/x` lives in
|
|
51488
|
-
* `feature-x`. Joining the
|
|
51489
|
-
* that does not exist -- which
|
|
51490
|
-
*
|
|
51491
|
-
*
|
|
51492
|
-
* wedging exactly the branch this workstream exists to unwedge.
|
|
51401
|
+
* `feature-x`. Joining the RAW name instead silently addresses a directory
|
|
51402
|
+
* that does not exist -- which drops the metadata writers' updates, and makes
|
|
51403
|
+
* the leased push read the history-rewrite marker as absent and go out
|
|
51404
|
+
* unleased, wedging the branch.
|
|
51493
51405
|
*
|
|
51494
|
-
* `name` inside the metadata itself stays the raw ref name; only the path
|
|
51495
|
-
*
|
|
51406
|
+
* `name` inside the metadata itself stays the raw ref name; only the path is
|
|
51407
|
+
* sanitized.
|
|
51496
51408
|
*/
|
|
51497
51409
|
branchWorkspacePath(branchRefName) {
|
|
51498
51410
|
return path17.join(this.contentBranchesPath, sanitizeBranchName(branchRefName));
|
|
@@ -51503,32 +51415,28 @@ var CmsWorker = class {
|
|
|
51503
51415
|
* `checkout --theirs` resolution loop overwrites them. No-op in production.
|
|
51504
51416
|
*
|
|
51505
51417
|
* A test subclass overrides this to land a real `ContentStore` write at
|
|
51506
|
-
* exactly the instant the rebase is mid-flight
|
|
51507
|
-
*
|
|
51508
|
-
* shell rendezvous. See the "Deterministic interleavings" testing pattern in
|
|
51418
|
+
* exactly the instant the rebase is mid-flight, with no sleeps or shell
|
|
51419
|
+
* rendezvous. See the "Deterministic interleavings" pattern in
|
|
51509
51420
|
* docs/concurrency.md, and `ContentStore.afterPrePassForTesting()` for the
|
|
51510
51421
|
* same idiom on the write side.
|
|
51511
51422
|
*/
|
|
51512
51423
|
async afterConflictDetectedForTesting() {
|
|
51513
51424
|
}
|
|
51514
51425
|
/**
|
|
51515
|
-
* Test seam, sibling of {@link afterConflictDetectedForTesting}: runs
|
|
51426
|
+
* Test seam, sibling of {@link afterConflictDetectedForTesting}: runs the
|
|
51516
51427
|
* instant a rebase round has succeeded, before the completion path (cache
|
|
51517
|
-
* invalidation, conflict metadata, [SYNC-H1] marker) executes
|
|
51518
|
-
*
|
|
51519
|
-
*
|
|
51428
|
+
* invalidation, conflict metadata, [SYNC-H1] marker) executes, so a test can
|
|
51429
|
+
* lose the content-write lock at exactly the point where bailing out would
|
|
51430
|
+
* strand a rewritten history.
|
|
51520
51431
|
*/
|
|
51521
51432
|
async afterRebaseCompletedForTesting() {
|
|
51522
51433
|
}
|
|
51523
51434
|
// --- Rebase loop (worker/rebase.ts) ------------------------------------
|
|
51524
51435
|
//
|
|
51525
51436
|
// Both are TEST-ONLY entry points -- see the task-queue block above. Nothing
|
|
51526
|
-
// in production dispatches through either
|
|
51527
|
-
// directly and that calls `pollMergeState` directly.
|
|
51528
|
-
//
|
|
51529
|
-
// drives), and cms-worker-merge-poll.test.ts calls `pollMergeState` with a
|
|
51530
|
-
// mocked `octokit`. Neither is on WorkerContext, so a stub installed on
|
|
51531
|
-
// either is a no-op as far as a production code path is concerned.
|
|
51437
|
+
// in production dispatches through either: `syncGit` calls `runRebaseCycle`
|
|
51438
|
+
// directly and that calls `pollMergeState` directly. Neither is on
|
|
51439
|
+
// WorkerContext, so a stub installed on either is a no-op for production.
|
|
51532
51440
|
//
|
|
51533
51441
|
// `rebaseActiveBranches` also has a consumer outside the test files:
|
|
51534
51442
|
// apps/test-app/app/api/e2e-test/rebase/route.ts drives it from an e2e
|
|
@@ -51540,14 +51448,11 @@ var CmsWorker = class {
|
|
|
51540
51448
|
//
|
|
51541
51449
|
// `syncGit` is the public loop entry `scheduleLoop` drives. The three private
|
|
51542
51450
|
// ones below it are TEST-ONLY entry points -- see the task-queue block above
|
|
51543
|
-
// -- each called directly by one test file
|
|
51544
|
-
// settings push, cms-worker-base-refresh for the base workspace,
|
|
51545
|
-
// cms-worker.test.ts for the trash sweep). `syncGit` reaches all three as
|
|
51451
|
+
// -- each called directly by one test file. `syncGit` reaches all three as
|
|
51546
51452
|
// module-level calls, so a stub installed on one of these is a no-op.
|
|
51547
51453
|
//
|
|
51548
51454
|
// `reconcileTrackedBranches` deliberately has NO delegator: no test reaches
|
|
51549
|
-
// it through the instance.
|
|
51550
|
-
// this file are stale by definition -- it is in git-sync.ts.
|
|
51455
|
+
// it through the instance.
|
|
51551
51456
|
async syncGit() {
|
|
51552
51457
|
return syncGit(this.ctx());
|
|
51553
51458
|
}
|
|
@@ -51556,10 +51461,10 @@ var CmsWorker = class {
|
|
|
51556
51461
|
*
|
|
51557
51462
|
* One of `refreshGitHubCredential`'s two call sites, and the one that works
|
|
51558
51463
|
* when nobody is publishing: it fetches from GitHub every `gitSyncInterval`
|
|
51559
|
-
*
|
|
51560
|
-
*
|
|
51464
|
+
* whether or not anyone is editing, so a credential that has stopped working
|
|
51465
|
+
* surfaces here even with no push queued for days.
|
|
51561
51466
|
*
|
|
51562
|
-
* The
|
|
51467
|
+
* The SYNC failure is what propagates to `scheduleLoop`'s catch;
|
|
51563
51468
|
* `refreshGitHubCredential` never throws, so nothing it does can replace it.
|
|
51564
51469
|
*/
|
|
51565
51470
|
async syncGitWithCredentialRefresh() {
|
|
@@ -51576,42 +51481,29 @@ var CmsWorker = class {
|
|
|
51576
51481
|
* **Two call sites, each covering what the other cannot.** The git-sync loop
|
|
51577
51482
|
* (`syncGitWithCredentialRefresh`) notices a dead credential when nobody is
|
|
51578
51483
|
* publishing. `processTaskQueue`'s per-task catch is what saves a publish: a
|
|
51579
|
-
* push task spends its retry budget on a 5s/10s/20s backoff
|
|
51580
|
-
*
|
|
51581
|
-
*
|
|
51582
|
-
*
|
|
51583
|
-
*
|
|
51584
|
-
*
|
|
51585
|
-
* reads it per use — the sync fetch, `pushBranchToGitHub`,
|
|
51586
|
-
* `pushSettingsBranches`, `ensureRemoteGit`'s clone, and every Octokit call —
|
|
51587
|
-
* so a refresh from either site repairs all of them for their NEXT use, with
|
|
51588
|
-
* nothing to invalidate. It cannot rescue an attempt that has already failed,
|
|
51589
|
-
* so a task `isPermanentTaskFailure` fails fast is not saved; the task after
|
|
51590
|
-
* it is.
|
|
51591
|
-
*
|
|
51592
|
-
* NOT gated on the error looking auth-shaped, at either site. There is nothing
|
|
51593
|
-
* to classify on: a `git fetch` or `git push` rejected for a dead token throws
|
|
51594
|
-
* a plain simple-git error — exit 128, no HTTP `.status` — which
|
|
51595
|
-
* `isPermanentTaskFailure` reads as transient, so a gate keyed on it would
|
|
51596
|
-
* never fire. Two floors bound the cost instead: core's own
|
|
51597
|
-
* `refreshGitHubTokenMinIntervalMs` (default 60s, enforced by
|
|
51598
|
-
* `refreshCredential` in github-auth.ts), and whatever floor the provider
|
|
51599
|
-
* keeps — the AWS one reads at most once per five minutes and returns
|
|
51600
|
-
* `undefined` for an unchanged value (canopycms-cdk/worker/credential-refresh.ts).
|
|
51601
|
-
* On the GitHub App path the refresh is a no-op. Both call sites share both
|
|
51602
|
-
* floors, so a read issued by one throttles the other.
|
|
51484
|
+
* push task spends its retry budget on a 5s/10s/20s backoff, well inside one
|
|
51485
|
+
* 5-minute sync interval, so with the sync loop as the only trigger a publish
|
|
51486
|
+
* meeting a rotated token fails permanently while the working one is already
|
|
51487
|
+
* in the secret store. Every consumer reaches the credential through
|
|
51488
|
+
* `ensureGitHubAuth()`, which reads it per use, so a refresh from either site
|
|
51489
|
+
* repairs all of them for their NEXT use — but not an attempt already failed.
|
|
51603
51490
|
*
|
|
51604
|
-
*
|
|
51605
|
-
*
|
|
51491
|
+
* NOT gated on the error looking auth-shaped, at either site: a `git
|
|
51492
|
+
* fetch`/`push` rejected for a dead token throws a plain simple-git error
|
|
51493
|
+
* (exit 128, no HTTP `.status`) that `isPermanentTaskFailure` reads as
|
|
51494
|
+
* transient, so a gate keyed on it would never fire. Two floors bound the
|
|
51495
|
+
* cost instead — core's `refreshGitHubTokenMinIntervalMs` (default 60s,
|
|
51496
|
+
* enforced by `refreshCredential` in github-auth.ts) and whatever floor the
|
|
51497
|
+
* provider keeps (the AWS one reads at most once per five minutes) — and both
|
|
51498
|
+
* call sites share both, so a read issued by one throttles the other. On the
|
|
51499
|
+
* GitHub App path the refresh is a no-op.
|
|
51606
51500
|
*
|
|
51607
|
-
* **
|
|
51608
|
-
*
|
|
51609
|
-
*
|
|
51610
|
-
* (
|
|
51611
|
-
* `getSecret`
|
|
51612
|
-
*
|
|
51613
|
-
* may still land later; `refreshCredential` discards a result older than one
|
|
51614
|
-
* it has already applied, so a late landing cannot put a stale token back.
|
|
51501
|
+
* **Never throws**: both callers are already reporting the failure that must
|
|
51502
|
+
* reach the log. **Bounded by `taskTimeoutMs`**, because the task loop awaits
|
|
51503
|
+
* it and a read that never settled would stop every publish queued behind it
|
|
51504
|
+
* (an adopter's provider may have no bound at all; the AWS one can take 87s
|
|
51505
|
+
* for one `getSecret`). A losing read is not cancelled and may land later, but
|
|
51506
|
+
* `refreshCredential` discards a result older than one already applied.
|
|
51615
51507
|
*/
|
|
51616
51508
|
async refreshGitHubCredential() {
|
|
51617
51509
|
let timer;
|
|
@@ -61321,12 +61213,9 @@ async function main() {
|
|
|
61321
61213
|
const githubAppAuth = githubAppId && githubAppInstallationId && githubAppPrivateKeySecretArn ? buildGitHubAppAuth({
|
|
61322
61214
|
appId: githubAppId,
|
|
61323
61215
|
installationId: githubAppInstallationId,
|
|
61324
|
-
//
|
|
61325
|
-
//
|
|
61326
|
-
//
|
|
61327
|
-
// two `getSecret` call sites in this file (the GitHub token above and
|
|
61328
|
-
// the Clerk key below), and for the same reason: a blank var means
|
|
61329
|
-
// "not configured", not "read the field named ''".
|
|
61216
|
+
// Same `|| undefined` as the other two `getSecret` call sites in this
|
|
61217
|
+
// file, for the same reason: a blank var means "not configured", not
|
|
61218
|
+
// "read the field named ''".
|
|
61330
61219
|
privateKey: await getSecret(githubAppPrivateKeySecretArn, {
|
|
61331
61220
|
jsonField: process.env.CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD || void 0,
|
|
61332
61221
|
jsonFieldEnvVar: "CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD"
|
|
@@ -61363,12 +61252,10 @@ async function main() {
|
|
|
61363
61252
|
refreshAuthCache,
|
|
61364
61253
|
baseBranch: process.env.CANOPYCMS_BASE_BRANCH ?? "main",
|
|
61365
61254
|
// deploymentName is deliberately NOT passed: CmsWorker resolves it through
|
|
61366
|
-
// resolveDeploymentName, which reads CANOPYCMS_DEPLOYMENT_NAME itself
|
|
61367
|
-
//
|
|
61368
|
-
//
|
|
61369
|
-
//
|
|
61370
|
-
// component. Reading the env var here instead would re-implement the
|
|
61371
|
-
// outer half of that chain and skip the validation.
|
|
61255
|
+
// resolveDeploymentName, which reads CANOPYCMS_DEPLOYMENT_NAME itself,
|
|
61256
|
+
// applies the same env > config > 'prod' precedence as the Lambda, and
|
|
61257
|
+
// validates the result as a git ref component. Reading the env var here
|
|
61258
|
+
// would re-implement the outer half of that chain and skip the validation.
|
|
61372
61259
|
// Explicit override, matching the strategy's own precedence: an adopter who
|
|
61373
61260
|
// sets `settingsBranch` in canopycms.config.ts must set this too, or the
|
|
61374
61261
|
// worker would own a branch name the Lambda never writes to.
|