@moinulmoin/eden 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +0 -9
- package/README.md +4 -4
- package/dist/eve-control-plane.d.ts +52 -12
- package/dist/eve-control-plane.js +641 -141
- package/dist/eve.js +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +3 -3
|
@@ -11,6 +11,7 @@ import { EveCliError, } from "./eve.js";
|
|
|
11
11
|
import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
|
|
12
12
|
import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
|
|
13
13
|
import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
|
|
14
|
+
import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
const wranglerEntrypoint = require.resolve("wrangler");
|
|
@@ -290,7 +291,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
290
291
|
if (result.ok) {
|
|
291
292
|
if (result.command === "eve deploy") {
|
|
292
293
|
const url = result.deployment?.stableWorkersDevOrigin;
|
|
293
|
-
lines.push("", `✓ deployed ${result.name} (${result.environment})` +
|
|
294
|
+
lines.push("", `✓ ${result.deployment?.operation === "update" ? "updated" : "deployed"} ${result.name} (${result.environment})` +
|
|
294
295
|
(eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
|
|
295
296
|
if (url !== undefined)
|
|
296
297
|
lines.push(url);
|
|
@@ -306,7 +307,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
306
307
|
lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
|
|
307
308
|
? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
|
|
308
309
|
: result.deployment.status === "deployed"
|
|
309
|
-
? "The deployment is live; only local runtime
|
|
310
|
+
? "The deployment is live; only image cleanup failed (local runtime image or the superseded registry tag), so do not retry the deployment."
|
|
310
311
|
: "The deployment failed before promotion; the exact target was not claimed."}`);
|
|
311
312
|
}
|
|
312
313
|
else if (firstFailure !== undefined) {
|
|
@@ -440,10 +441,13 @@ function exactContainerEntries(value, name) {
|
|
|
440
441
|
typeof entry.name !== "string") {
|
|
441
442
|
return undefined;
|
|
442
443
|
}
|
|
443
|
-
const id = entry.id;
|
|
444
444
|
const entryName = entry.name;
|
|
445
|
-
if (entryName === name)
|
|
446
|
-
entries.push({
|
|
445
|
+
if (entryName === name) {
|
|
446
|
+
entries.push({
|
|
447
|
+
id: entry.id,
|
|
448
|
+
name: entryName,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
447
451
|
}
|
|
448
452
|
if (entries.length === 0 && collection.length >= 100) {
|
|
449
453
|
return undefined;
|
|
@@ -595,6 +599,9 @@ async function defaultCloudflareRead(request) {
|
|
|
595
599
|
target: { state: "absent" },
|
|
596
600
|
};
|
|
597
601
|
}
|
|
602
|
+
const workerPresent = deploymentEntries.length > 0;
|
|
603
|
+
const containerPresent = (containerEntries?.length ?? 0) > 0;
|
|
604
|
+
const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0] : undefined);
|
|
598
605
|
return {
|
|
599
606
|
accountAccess,
|
|
600
607
|
containerAccess,
|
|
@@ -605,16 +612,175 @@ async function defaultCloudflareRead(request) {
|
|
|
605
612
|
? {}
|
|
606
613
|
: { workersDevSubdomain }),
|
|
607
614
|
target: {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
615
|
+
// The read-only inventory proves the exact target exists but cannot
|
|
616
|
+
// prove who created it; Eden's immutable deployment record resolves
|
|
617
|
+
// ownership next.
|
|
618
|
+
state: "unowned",
|
|
619
|
+
observed: { worker: workerPresent, container: containerPresent },
|
|
620
|
+
...(providerEvidence === undefined ? {} : { providerEvidence }),
|
|
621
|
+
message: "The exact Worker or Container target already exists, and the read-only inventory cannot prove Eden owns it.",
|
|
622
|
+
remediation: "Deploy under a different --name or remove the conflicting Worker/Container yourself; Eden never adopts or replaces resources it cannot prove it created.",
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Reads provider-backed facts about the live target: the Container
|
|
628
|
+
* application's identity from `wrangler containers list` plus `containers
|
|
629
|
+
* info` (scheduling policy and Durable Object namespace), and the
|
|
630
|
+
* `EDEN_EVE_DEPLOYMENT_ID` plain-text var plus the `EVE_CONTAINER` Durable
|
|
631
|
+
* Object namespace Eden stamps into every Worker it publishes (from the
|
|
632
|
+
* script settings API). All are provider state — a foreign Worker can forge
|
|
633
|
+
* `x-eden-*` response headers but cannot set vars or namespaces Eden
|
|
634
|
+
* recorded at publish time. The ownership proof is the Container
|
|
635
|
+
* application's Durable Object namespace matching the Worker's binding
|
|
636
|
+
* namespace.
|
|
637
|
+
*/
|
|
638
|
+
async function readProviderEvidence(workerName, accountId, workerPresent, containerEntry) {
|
|
639
|
+
const evidence = {
|
|
640
|
+
...(containerEntry === undefined
|
|
641
|
+
? {}
|
|
642
|
+
: { containerApplicationId: containerEntry.id }),
|
|
643
|
+
};
|
|
644
|
+
if (containerEntry !== undefined) {
|
|
645
|
+
const info = await readWranglerJson([
|
|
646
|
+
"containers",
|
|
647
|
+
"info",
|
|
648
|
+
containerEntry.id,
|
|
649
|
+
"--json",
|
|
650
|
+
]);
|
|
651
|
+
if (!info.failed && typeof info.value === "object" && info.value !== null) {
|
|
652
|
+
const infoRecord = info.value;
|
|
653
|
+
if (typeof infoRecord.scheduling_policy === "string") {
|
|
654
|
+
evidence.containerSchedulingPolicy = infoRecord.scheduling_policy;
|
|
655
|
+
}
|
|
656
|
+
const namespaceId = infoRecord.durable_objects?.namespace_id;
|
|
657
|
+
if (typeof namespaceId === "string") {
|
|
658
|
+
evidence.containerNamespaceId = namespaceId;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (workerPresent && accountId !== undefined) {
|
|
663
|
+
const token = process.env.CLOUDFLARE_API_TOKEN ??
|
|
664
|
+
(await readWranglerOAuthToken());
|
|
665
|
+
if (token !== undefined) {
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${encodeURIComponent(workerName)}/settings`, {
|
|
668
|
+
headers: { authorization: `Bearer ${token}` },
|
|
669
|
+
signal: AbortSignal.timeout(20_000),
|
|
670
|
+
});
|
|
671
|
+
if (response.ok) {
|
|
672
|
+
evidence.settingsReadable = true;
|
|
673
|
+
const parsed = (await response.json());
|
|
674
|
+
const bindings = parsed.result?.bindings ?? [];
|
|
675
|
+
const deploymentBinding = bindings.find((value) => value.type === "plain_text" &&
|
|
676
|
+
value.name === "EDEN_EVE_DEPLOYMENT_ID");
|
|
677
|
+
if (typeof deploymentBinding?.text === "string") {
|
|
678
|
+
evidence.workerDeploymentId = deploymentBinding.text;
|
|
679
|
+
}
|
|
680
|
+
const containerBinding = bindings.find((value) => value.type === "durable_object_namespace" &&
|
|
681
|
+
value.name === "EVE_CONTAINER");
|
|
682
|
+
if (typeof containerBinding?.namespace_id === "string") {
|
|
683
|
+
evidence.workerContainerNamespaceId = containerBinding.namespace_id;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (response.status !== 404) {
|
|
687
|
+
evidence.settingsReadable = false;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
evidence.settingsReadable = false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return evidence;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Resolves `unowned` reads against Eden's immutable deployment records AND
|
|
699
|
+
* the live remote target. The local record proves Eden once created the
|
|
700
|
+
* target; the remote Worker's stamped identity proves it is still the one
|
|
701
|
+
* Eden deployed — a same-name Worker recreated by something else after the
|
|
702
|
+
* target was destroyed (or a forged local record) must never be upgraded to
|
|
703
|
+
* `owned`.
|
|
704
|
+
*/
|
|
705
|
+
async function resolveEveTargetOwnership(request, result) {
|
|
706
|
+
if (result.target.state !== "unowned")
|
|
707
|
+
return result;
|
|
708
|
+
const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, result.accountId, request.projectId === "unknown-project" ? undefined : request.projectId);
|
|
709
|
+
if (record === undefined ||
|
|
710
|
+
record.identity.workerName !== request.name ||
|
|
711
|
+
record.identity.accountId !== result.accountId ||
|
|
712
|
+
(request.projectId !== "unknown-project" &&
|
|
713
|
+
record.identity.projectId !== undefined &&
|
|
714
|
+
record.identity.projectId !== request.projectId) ||
|
|
715
|
+
(result.target.observed?.container === true &&
|
|
716
|
+
record.identity.containerApplicationName !==
|
|
717
|
+
boundedResourceName(request.name, "container"))) {
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
// Remote proof must come from provider state. A foreign Worker can echo
|
|
721
|
+
// `x-eden-*` response headers, but it cannot plant the plain-text var Eden
|
|
722
|
+
// publishes into script settings or claim the Durable Object namespace the
|
|
723
|
+
// Worker's EVE_CONTAINER binding resolves to. The Worker is proven only by
|
|
724
|
+
// its own settings: unreadable settings fail closed.
|
|
725
|
+
const evidence = result.target.providerEvidence;
|
|
726
|
+
const recordedDeploymentIds = new Set(record.deploymentIds);
|
|
727
|
+
const workerProven = result.target.observed?.worker !== true ||
|
|
728
|
+
(evidence?.settingsReadable === true &&
|
|
729
|
+
evidence.workerDeploymentId !== undefined &&
|
|
730
|
+
recordedDeploymentIds.has(evidence.workerDeploymentId));
|
|
731
|
+
// The Container proof is the application running the durable_object
|
|
732
|
+
// scheduling policy with a namespace that matches the Worker binding's
|
|
733
|
+
// namespace; anything else fails closed.
|
|
734
|
+
const containerProven = result.target.observed?.container !== true ||
|
|
735
|
+
(evidence?.containerSchedulingPolicy === "durable_object" &&
|
|
736
|
+
evidence.containerNamespaceId !== undefined &&
|
|
737
|
+
evidence.workerContainerNamespaceId !== undefined &&
|
|
738
|
+
evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
|
|
739
|
+
const remoteProven = evidence !== undefined && workerProven && containerProven;
|
|
740
|
+
if (!remoteProven) {
|
|
741
|
+
return {
|
|
742
|
+
...result,
|
|
743
|
+
target: {
|
|
744
|
+
...result.target,
|
|
745
|
+
message: "The exact Worker or Container target exists, and although a local Eden record matches, provider state cannot prove it is the one Eden deployed — the target may have been recreated outside Eden.",
|
|
746
|
+
remediation: "Inspect the Worker at its workers.dev origin and the recorded deployment history before redeploying; if it is not Eden's, deploy under a different --name or delete it yourself.",
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const priorIdentity = {
|
|
751
|
+
workerName: record.identity.workerName,
|
|
752
|
+
containerApplicationName: record.identity.containerApplicationName,
|
|
753
|
+
stableContainerInstanceName: record.identity.stableContainerInstanceName,
|
|
754
|
+
accountId: record.identity.accountId,
|
|
755
|
+
environment: request.environment,
|
|
756
|
+
name: request.name,
|
|
757
|
+
...(record.identity.projectId === undefined
|
|
758
|
+
? {}
|
|
759
|
+
: { projectId: record.identity.projectId }),
|
|
760
|
+
...(record.identity.generationId === undefined
|
|
761
|
+
? {}
|
|
762
|
+
: { generationId: record.identity.generationId }),
|
|
763
|
+
...(record.identity.containerImage === undefined
|
|
764
|
+
? {}
|
|
765
|
+
: { containerImage: record.identity.containerImage }),
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
...result,
|
|
769
|
+
target: {
|
|
770
|
+
state: "owned",
|
|
771
|
+
identity: priorIdentity,
|
|
772
|
+
...(result.target.observed === undefined
|
|
773
|
+
? {}
|
|
774
|
+
: { observed: result.target.observed }),
|
|
775
|
+
message: "The immutable deployment record proves Eden owns the exact target; deploy proceeds as an in-place update.",
|
|
611
776
|
},
|
|
612
777
|
};
|
|
613
778
|
}
|
|
614
779
|
async function readEveCloudflareTarget(request, options) {
|
|
615
|
-
|
|
616
|
-
? defaultCloudflareRead(request)
|
|
617
|
-
: options.cloudflareRead(request);
|
|
780
|
+
const result = options.cloudflareRead === undefined
|
|
781
|
+
? await defaultCloudflareRead(request)
|
|
782
|
+
: await options.cloudflareRead(request);
|
|
783
|
+
return resolveEveTargetOwnership(request, result);
|
|
618
784
|
}
|
|
619
785
|
async function resolvedCloudflareOrigin(result, workerName) {
|
|
620
786
|
if (result.accountId === undefined ||
|
|
@@ -622,7 +788,6 @@ async function resolvedCloudflareOrigin(result, workerName) {
|
|
|
622
788
|
return undefined;
|
|
623
789
|
}
|
|
624
790
|
try {
|
|
625
|
-
const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
626
791
|
const origin = resolveStableWorkersDevOrigin({
|
|
627
792
|
workerName,
|
|
628
793
|
workersDevSubdomain: result.workersDevSubdomain,
|
|
@@ -713,12 +878,12 @@ function cloudflareChecks(result) {
|
|
|
713
878
|
? undefined
|
|
714
879
|
: "Use an authenticated read-only adapter with account and Container access; preflight never uploads, publishes, or changes remote state.";
|
|
715
880
|
const target = result.target;
|
|
716
|
-
const targetPasses = target.state === "absent" ||
|
|
717
|
-
(target.state === "owned" && target.matchesCandidate === true);
|
|
881
|
+
const targetPasses = target.state === "absent" || target.state === "owned";
|
|
718
882
|
const targetMessage = targetPasses
|
|
719
883
|
? target.state === "absent"
|
|
720
884
|
? "The exact target read is absent and is eligible without claiming ownership."
|
|
721
|
-
:
|
|
885
|
+
: target.message ??
|
|
886
|
+
"The exact target is provably Eden-owned; deploy proceeds as an in-place update."
|
|
722
887
|
: target.message ??
|
|
723
888
|
"The exact target read is unowned, mismatched, cross-environment, or ambiguous.";
|
|
724
889
|
const targetRemediation = targetPasses
|
|
@@ -989,8 +1154,6 @@ function defaultPublicationRunner() {
|
|
|
989
1154
|
"--env",
|
|
990
1155
|
request.identity.environment,
|
|
991
1156
|
"--strict",
|
|
992
|
-
"--containers-rollout",
|
|
993
|
-
"immediate",
|
|
994
1157
|
], dirname(request.wranglerConfigPath));
|
|
995
1158
|
if (result.exitCode !== 0) {
|
|
996
1159
|
return {
|
|
@@ -1062,6 +1225,10 @@ function responseIdentity(response, body) {
|
|
|
1062
1225
|
read("workerName", "x-eden-eve-worker-name");
|
|
1063
1226
|
read("containerApplicationName", "x-eden-eve-container-application");
|
|
1064
1227
|
read("stableContainerInstanceName", "x-eden-eve-container-instance");
|
|
1228
|
+
const started = response.headers.get("x-eden-eve-started-deployment");
|
|
1229
|
+
if (started !== null) {
|
|
1230
|
+
identity["startedDeploymentId"] = started;
|
|
1231
|
+
}
|
|
1065
1232
|
read("containerImage", "x-eden-eve-container-image");
|
|
1066
1233
|
return identity;
|
|
1067
1234
|
}
|
|
@@ -1105,9 +1272,20 @@ function defaultHealthRunner() {
|
|
|
1105
1272
|
body.state === "ready" ||
|
|
1106
1273
|
body.ready === true);
|
|
1107
1274
|
if (ready) {
|
|
1275
|
+
const observedIdentity = responseIdentity(response, body);
|
|
1276
|
+
// A Worker activates before its image rollout completes; promotion
|
|
1277
|
+
// must wait until the *served container* verifiably runs this
|
|
1278
|
+
// generation's image (the host stamps the header only after
|
|
1279
|
+
// inspecting the running instance).
|
|
1280
|
+
if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
|
|
1281
|
+
lastReason =
|
|
1282
|
+
"The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
|
|
1283
|
+
await wait();
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1108
1286
|
return {
|
|
1109
1287
|
status: "ready",
|
|
1110
|
-
identity:
|
|
1288
|
+
identity: observedIdentity,
|
|
1111
1289
|
};
|
|
1112
1290
|
}
|
|
1113
1291
|
lastReason =
|
|
@@ -1161,8 +1339,13 @@ async function releaseEveTargetLock(lock) {
|
|
|
1161
1339
|
await rm(lock.path, { force: true });
|
|
1162
1340
|
return (await lstat(lock.path).catch(() => undefined)) === undefined;
|
|
1163
1341
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1342
|
+
/**
|
|
1343
|
+
* Atomically re-points the target's CURRENT symlink at a promoted immutable
|
|
1344
|
+
* generation. The pointer key is `<environment>-<name>` — the same path
|
|
1345
|
+
* `destroy` clears and the update ownership resolver reads.
|
|
1346
|
+
*/
|
|
1347
|
+
async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoot) {
|
|
1348
|
+
const targetRoot = join(deploymentRoot, "targets", pointerKey);
|
|
1166
1349
|
const artifactBoundary = await realpath(dirname(deploymentRoot)).catch(() => undefined);
|
|
1167
1350
|
const generationCanonical = await realpath(generationRoot).catch(() => undefined);
|
|
1168
1351
|
if (artifactBoundary === undefined ||
|
|
@@ -1191,7 +1374,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1191
1374
|
throw new EveCliError({
|
|
1192
1375
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1193
1376
|
message: "The Eve target pointer directory could not be verified safely.",
|
|
1194
|
-
source:
|
|
1377
|
+
source: pointerKey,
|
|
1195
1378
|
});
|
|
1196
1379
|
}
|
|
1197
1380
|
const currentPath = join(targetRoot, "CURRENT");
|
|
@@ -1208,7 +1391,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1208
1391
|
throw new EveCliError({
|
|
1209
1392
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1210
1393
|
message: "The Eve target pointer did not resolve to the promoted generation.",
|
|
1211
|
-
source:
|
|
1394
|
+
source: pointerKey,
|
|
1212
1395
|
});
|
|
1213
1396
|
}
|
|
1214
1397
|
return currentPath;
|
|
@@ -1242,9 +1425,10 @@ async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
|
|
|
1242
1425
|
flag: "wx",
|
|
1243
1426
|
});
|
|
1244
1427
|
}
|
|
1245
|
-
function deploymentMetadata(identity, targetKey, status) {
|
|
1428
|
+
function deploymentMetadata(identity, targetKey, status, operation, superseded) {
|
|
1246
1429
|
return {
|
|
1247
1430
|
status,
|
|
1431
|
+
operation,
|
|
1248
1432
|
targetKey,
|
|
1249
1433
|
deploymentId: identity.deploymentId,
|
|
1250
1434
|
workerName: identity.workerName,
|
|
@@ -1252,6 +1436,12 @@ function deploymentMetadata(identity, targetKey, status) {
|
|
|
1252
1436
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1253
1437
|
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1254
1438
|
imageReference: identity.containerImage,
|
|
1439
|
+
...(superseded?.generationId === undefined
|
|
1440
|
+
? {}
|
|
1441
|
+
: { supersededGenerationId: superseded.generationId }),
|
|
1442
|
+
...(superseded?.containerImage === undefined
|
|
1443
|
+
? {}
|
|
1444
|
+
: { supersededImageReference: superseded.containerImage }),
|
|
1255
1445
|
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1256
1446
|
evidenceRetained: true,
|
|
1257
1447
|
};
|
|
@@ -1273,6 +1463,46 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1273
1463
|
collected.projectId === undefined) {
|
|
1274
1464
|
throw deploymentFailure("EVE_DEPLOY_CHECKS_FAILED", "The inline Eve deploy checks did not return a complete immutable candidate.");
|
|
1275
1465
|
}
|
|
1466
|
+
const isUpdate = cloudflare.target.state === "owned";
|
|
1467
|
+
const operation = isUpdate ? "update" : "create";
|
|
1468
|
+
const prior = collected.priorDeployment;
|
|
1469
|
+
if (isUpdate && prior === undefined) {
|
|
1470
|
+
throw deploymentFailure("EVE_UPDATE_RECORD_UNAVAILABLE", "The exact target is provably Eden-owned, but its immutable deployment record is unreadable; refusing to update without the recorded Durable Object history.");
|
|
1471
|
+
}
|
|
1472
|
+
if (isUpdate && prior?.durableObjects === undefined) {
|
|
1473
|
+
throw deploymentFailure("EVE_UPDATE_RECORD_UNAVAILABLE", "The prior deployment record does not carry a readable Durable Object history; refusing to update because replacing it could emit a divergent migration list.");
|
|
1474
|
+
}
|
|
1475
|
+
const priorIdentity = prior?.identity;
|
|
1476
|
+
const priorDurableObjects = prior?.durableObjects;
|
|
1477
|
+
const superseded = isUpdate
|
|
1478
|
+
? {
|
|
1479
|
+
generationId: priorIdentity?.generationId,
|
|
1480
|
+
containerImage: priorIdentity?.containerImage,
|
|
1481
|
+
}
|
|
1482
|
+
: undefined;
|
|
1483
|
+
const resultWarnings = collected.result.warnings;
|
|
1484
|
+
if (isUpdate) {
|
|
1485
|
+
const world = collected.workflowWorld;
|
|
1486
|
+
const durableWorld = world !== undefined &&
|
|
1487
|
+
world !== "local" &&
|
|
1488
|
+
world !== "@workflow/world-local";
|
|
1489
|
+
if (durableWorld) {
|
|
1490
|
+
resultWarnings.push({
|
|
1491
|
+
id: "EVE_UPDATE_IN_FLIGHT",
|
|
1492
|
+
message: "This update replaces the running Container; durable World runs in flight resume on the new code — Cloudflare has no deployment pinning, so keep step names and payload shapes backward compatible.",
|
|
1493
|
+
remediation: "Let pending approvals and long runs settle before updating, or keep step names, queue names, and serialized payloads compatible across the change.",
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
if (prior?.worldCloudflare !==
|
|
1497
|
+
(world === "@moinulmoin/eden-world-cloudflare")) {
|
|
1498
|
+
resultWarnings.push({
|
|
1499
|
+
id: "EVE_UPDATE_WORLD_SWITCH",
|
|
1500
|
+
message: "The compiled Workflow World changed since the last deploy; existing sessions and pending approvals stay in the previous World and do not migrate to the new one.",
|
|
1501
|
+
remediation: "Existing Durable Object data is preserved (classes and bindings are never removed), but runs started before this update remain where they were created.",
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
eveProgress(options, "update: Durable Object data is preserved; the running Container restarts on its next request to serve the new image");
|
|
1505
|
+
}
|
|
1276
1506
|
const accountId = cloudflare.accountId ??
|
|
1277
1507
|
cloudflare.target.identity?.accountId;
|
|
1278
1508
|
const workersDevSubdomain = cloudflare.workersDevSubdomain ??
|
|
@@ -1282,7 +1512,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1282
1512
|
}
|
|
1283
1513
|
let stableWorkersDevOrigin;
|
|
1284
1514
|
try {
|
|
1285
|
-
const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1286
1515
|
stableWorkersDevOrigin = resolveStableWorkersDevOrigin({
|
|
1287
1516
|
workerName: request.name,
|
|
1288
1517
|
workersDevSubdomain,
|
|
@@ -1399,31 +1628,39 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1399
1628
|
? {}
|
|
1400
1629
|
: { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
|
|
1401
1630
|
};
|
|
1631
|
+
const baseConfigRequest = {
|
|
1632
|
+
accountId,
|
|
1633
|
+
workerName: identity.workerName,
|
|
1634
|
+
containerApplicationName: identity.containerApplicationName,
|
|
1635
|
+
containerClassName: "EveHostDurableContainer",
|
|
1636
|
+
containerBindingName: "EVE_CONTAINER",
|
|
1637
|
+
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1638
|
+
deploymentId: identity.deploymentId,
|
|
1639
|
+
generationId: identity.generationId,
|
|
1640
|
+
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1641
|
+
containerImage: identity.containerImage,
|
|
1642
|
+
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1643
|
+
...(identity.runtimeRevisionHandle === undefined
|
|
1644
|
+
? {}
|
|
1645
|
+
: { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
|
|
1646
|
+
schedules: collected.schedules ?? [],
|
|
1647
|
+
...(collected.workflowWorld === undefined
|
|
1648
|
+
? {}
|
|
1649
|
+
: { workflowWorld: collected.workflowWorld }),
|
|
1650
|
+
// In-place updates carry the prior Durable Object bindings and
|
|
1651
|
+
// migration history forward verbatim: classes are append-only and are
|
|
1652
|
+
// never silently re-declared or removed.
|
|
1653
|
+
...(isUpdate && priorDurableObjects !== undefined
|
|
1654
|
+
? { durableObjects: priorDurableObjects }
|
|
1655
|
+
: {}),
|
|
1656
|
+
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1657
|
+
? {}
|
|
1658
|
+
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
1659
|
+
};
|
|
1402
1660
|
let hostConfig;
|
|
1403
1661
|
try {
|
|
1404
|
-
const { createEveHostConfig, } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1405
1662
|
hostConfig = createEveHostConfig({
|
|
1406
|
-
|
|
1407
|
-
workerName: identity.workerName,
|
|
1408
|
-
containerApplicationName: identity.containerApplicationName,
|
|
1409
|
-
containerClassName: "EveHostContainer",
|
|
1410
|
-
containerBindingName: "EVE_CONTAINER",
|
|
1411
|
-
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1412
|
-
deploymentId: identity.deploymentId,
|
|
1413
|
-
generationId: identity.generationId,
|
|
1414
|
-
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1415
|
-
containerImage: identity.containerImage,
|
|
1416
|
-
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1417
|
-
...(identity.runtimeRevisionHandle === undefined
|
|
1418
|
-
? {}
|
|
1419
|
-
: { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
|
|
1420
|
-
schedules: collected.schedules ?? [],
|
|
1421
|
-
...(collected.workflowWorld === undefined
|
|
1422
|
-
? {}
|
|
1423
|
-
: { workflowWorld: collected.workflowWorld }),
|
|
1424
|
-
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1425
|
-
? {}
|
|
1426
|
-
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
1663
|
+
...baseConfigRequest,
|
|
1427
1664
|
});
|
|
1428
1665
|
}
|
|
1429
1666
|
catch (error) {
|
|
@@ -1431,7 +1668,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1431
1668
|
? error.message
|
|
1432
1669
|
: "The exact Eve Worker/Container host configuration was invalid.");
|
|
1433
1670
|
}
|
|
1434
|
-
const { generateEveHostWorkerSource } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1435
1671
|
const workerSource = generateEveHostWorkerSource({ config: hostConfig });
|
|
1436
1672
|
const paths = await writeEveDeploymentArtifacts(candidate, hostConfig, workerSource);
|
|
1437
1673
|
const publicationRequest = {
|
|
@@ -1441,14 +1677,27 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1441
1677
|
...paths,
|
|
1442
1678
|
...(runtimeInjection === undefined ? {} : { runtimeInjection }),
|
|
1443
1679
|
};
|
|
1680
|
+
/** Publishes one wrangler config; returns its terminal result. */
|
|
1681
|
+
const publishOne = async (request) => (options.publish ?? defaultPublicationRunner())(request);
|
|
1444
1682
|
let publication;
|
|
1683
|
+
const writeFailureRecord = async (status, evidence) => {
|
|
1684
|
+
try {
|
|
1685
|
+
await writeEveDeploymentRecord(candidate, identity, hostConfig, status, {
|
|
1686
|
+
evidence,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
catch (error) {
|
|
1690
|
+
if (error.code !== "EEXIST")
|
|
1691
|
+
throw error;
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1445
1694
|
try {
|
|
1446
1695
|
eveProgress(options, "publishing Worker and Container application");
|
|
1447
|
-
publication = await (
|
|
1696
|
+
publication = await publishOne(publicationRequest);
|
|
1448
1697
|
}
|
|
1449
1698
|
catch {
|
|
1450
|
-
await
|
|
1451
|
-
const deployment = deploymentMetadata(identity, targetKey, "indeterminate");
|
|
1699
|
+
await writeFailureRecord("indeterminate", "publication-threw-before-terminal-result");
|
|
1700
|
+
const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
|
|
1452
1701
|
return {
|
|
1453
1702
|
...collected.result,
|
|
1454
1703
|
ok: false,
|
|
@@ -1456,11 +1705,11 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1456
1705
|
};
|
|
1457
1706
|
}
|
|
1458
1707
|
if (publication.status === "indeterminate") {
|
|
1459
|
-
await
|
|
1708
|
+
await writeFailureRecord("indeterminate", safeText(publication.reason));
|
|
1460
1709
|
return {
|
|
1461
1710
|
...collected.result,
|
|
1462
1711
|
ok: false,
|
|
1463
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1712
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1464
1713
|
};
|
|
1465
1714
|
}
|
|
1466
1715
|
if (publication.status === "failed" ||
|
|
@@ -1472,7 +1721,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1472
1721
|
return {
|
|
1473
1722
|
...collected.result,
|
|
1474
1723
|
ok: false,
|
|
1475
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1724
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1476
1725
|
};
|
|
1477
1726
|
}
|
|
1478
1727
|
if (publication.status === "failed" &&
|
|
@@ -1483,7 +1732,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1483
1732
|
return {
|
|
1484
1733
|
...collected.result,
|
|
1485
1734
|
ok: false,
|
|
1486
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1735
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1487
1736
|
};
|
|
1488
1737
|
}
|
|
1489
1738
|
if (publication.status === "failed" &&
|
|
@@ -1501,7 +1750,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1501
1750
|
return {
|
|
1502
1751
|
...collected.result,
|
|
1503
1752
|
ok: false,
|
|
1504
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1753
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1505
1754
|
};
|
|
1506
1755
|
}
|
|
1507
1756
|
}
|
|
@@ -1513,7 +1762,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1513
1762
|
return {
|
|
1514
1763
|
...collected.result,
|
|
1515
1764
|
ok: false,
|
|
1516
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1765
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1517
1766
|
};
|
|
1518
1767
|
}
|
|
1519
1768
|
let health;
|
|
@@ -1529,7 +1778,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1529
1778
|
return {
|
|
1530
1779
|
...collected.result,
|
|
1531
1780
|
ok: false,
|
|
1532
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1781
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1533
1782
|
};
|
|
1534
1783
|
}
|
|
1535
1784
|
if (health.status === "indeterminate") {
|
|
@@ -1537,7 +1786,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1537
1786
|
return {
|
|
1538
1787
|
...collected.result,
|
|
1539
1788
|
ok: false,
|
|
1540
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1789
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1541
1790
|
};
|
|
1542
1791
|
}
|
|
1543
1792
|
const observedHealthIdentity = health.status === "ready"
|
|
@@ -1554,7 +1803,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1554
1803
|
return {
|
|
1555
1804
|
...collected.result,
|
|
1556
1805
|
ok: false,
|
|
1557
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1806
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1558
1807
|
};
|
|
1559
1808
|
}
|
|
1560
1809
|
if (publication.createdByAttempt === true &&
|
|
@@ -1564,7 +1813,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1564
1813
|
return {
|
|
1565
1814
|
...collected.result,
|
|
1566
1815
|
ok: false,
|
|
1567
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1816
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1568
1817
|
};
|
|
1569
1818
|
}
|
|
1570
1819
|
if (publication.createdByAttempt === true &&
|
|
@@ -1578,7 +1827,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1578
1827
|
return {
|
|
1579
1828
|
...collected.result,
|
|
1580
1829
|
ok: false,
|
|
1581
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1830
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1582
1831
|
};
|
|
1583
1832
|
}
|
|
1584
1833
|
}
|
|
@@ -1590,7 +1839,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1590
1839
|
return {
|
|
1591
1840
|
...collected.result,
|
|
1592
1841
|
ok: false,
|
|
1593
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1842
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1594
1843
|
};
|
|
1595
1844
|
}
|
|
1596
1845
|
try {
|
|
@@ -1609,7 +1858,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1609
1858
|
return {
|
|
1610
1859
|
...collected.result,
|
|
1611
1860
|
ok: false,
|
|
1612
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1861
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1613
1862
|
};
|
|
1614
1863
|
}
|
|
1615
1864
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "failed", {
|
|
@@ -1620,7 +1869,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1620
1869
|
return {
|
|
1621
1870
|
...collected.result,
|
|
1622
1871
|
ok: false,
|
|
1623
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1872
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1624
1873
|
};
|
|
1625
1874
|
}
|
|
1626
1875
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", {
|
|
@@ -1631,7 +1880,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1631
1880
|
return {
|
|
1632
1881
|
...collected.result,
|
|
1633
1882
|
ok: false,
|
|
1634
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1883
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1635
1884
|
};
|
|
1636
1885
|
}
|
|
1637
1886
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "deployed", {
|
|
@@ -1641,8 +1890,34 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1641
1890
|
identity: "exact",
|
|
1642
1891
|
},
|
|
1643
1892
|
});
|
|
1644
|
-
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"),
|
|
1893
|
+
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
|
|
1645
1894
|
await options.afterPromotion?.();
|
|
1895
|
+
let supersededImageCleanup;
|
|
1896
|
+
if (isUpdate) {
|
|
1897
|
+
// The superseded generation's registry repository is generation-scoped
|
|
1898
|
+
// and no longer referenced once CURRENT promotes this generation; only
|
|
1899
|
+
// then is it safe to delete. Every tag in the repository goes with it —
|
|
1900
|
+
// including the snapshot tags Cloudflare created under it.
|
|
1901
|
+
const previousRepository = priorIdentity === undefined
|
|
1902
|
+
? undefined
|
|
1903
|
+
: ownedRegistryRepository(priorIdentity, request.environment, request.name, accountId);
|
|
1904
|
+
if (previousRepository !== undefined) {
|
|
1905
|
+
eveProgress(options, `deleting superseded registry repository ${previousRepository}`);
|
|
1906
|
+
let deleted = "indeterminate";
|
|
1907
|
+
try {
|
|
1908
|
+
deleted = await (options.deleteRegistryRepository ??
|
|
1909
|
+
defaultRegistryRepositoryDelete)({ repository: previousRepository });
|
|
1910
|
+
}
|
|
1911
|
+
catch {
|
|
1912
|
+
deleted = "indeterminate";
|
|
1913
|
+
}
|
|
1914
|
+
supersededImageCleanup = deleted === "indeterminate"
|
|
1915
|
+
? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry repository ${previousRepository} could not be proven deleted.`, `Remove only the recorded repository's tags ${previousRepository}; do not retry or run broad registry cleanup.`)
|
|
1916
|
+
: check("VAL-LIFE-007", "passed", deleted === "deleted"
|
|
1917
|
+
? "The superseded generation's registry repository (all tags, including snapshots) was deleted after promotion."
|
|
1918
|
+
: "The superseded generation's registry repository was already absent.");
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1646
1921
|
let runtimeImageCleanup;
|
|
1647
1922
|
if (runtimeEvidence.cleanup.imageRetained) {
|
|
1648
1923
|
if (options.retainRuntimeImage === true) {
|
|
@@ -1666,13 +1941,18 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1666
1941
|
: check("VAL-CROSS-004", "failed", "The remote deployment is healthy, but exact local runtime-image cleanup could not be verified.", `Remove only the recorded image ${imageDigest}; do not retry or run broad Docker cleanup.`);
|
|
1667
1942
|
}
|
|
1668
1943
|
}
|
|
1944
|
+
const postPromotionChecks = [
|
|
1945
|
+
...(supersededImageCleanup === undefined ? [] : [supersededImageCleanup]),
|
|
1946
|
+
...(runtimeImageCleanup === undefined ? [] : [runtimeImageCleanup]),
|
|
1947
|
+
];
|
|
1669
1948
|
return {
|
|
1670
1949
|
...collected.result,
|
|
1671
|
-
ok: runtimeImageCleanup?.status !== "failed"
|
|
1672
|
-
|
|
1950
|
+
ok: runtimeImageCleanup?.status !== "failed" &&
|
|
1951
|
+
supersededImageCleanup?.status !== "failed",
|
|
1952
|
+
...(postPromotionChecks.length === 0
|
|
1673
1953
|
? {}
|
|
1674
|
-
: { checks: [...collected.result.checks,
|
|
1675
|
-
deployment: deploymentMetadata(identity, targetKey, "deployed"),
|
|
1954
|
+
: { checks: [...collected.result.checks, ...postPromotionChecks] }),
|
|
1955
|
+
deployment: deploymentMetadata(identity, targetKey, "deployed", operation, superseded),
|
|
1676
1956
|
};
|
|
1677
1957
|
}
|
|
1678
1958
|
async function collectEvePreflight(request, options) {
|
|
@@ -1813,9 +2093,6 @@ async function collectEvePreflight(request, options) {
|
|
|
1813
2093
|
}
|
|
1814
2094
|
warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
|
|
1815
2095
|
const schedules = await readCompiledSchedules(candidate);
|
|
1816
|
-
// The runtime package is imported lazily so `eden` commands that never
|
|
1817
|
-
// reach deploy checks stay fast; the module specifier is fixed.
|
|
1818
|
-
const { parseEveScheduleCron } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1819
2096
|
const expressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) !== undefined);
|
|
1820
2097
|
const unexpressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) === undefined);
|
|
1821
2098
|
if (unexpressible.length > 0) {
|
|
@@ -1848,12 +2125,14 @@ async function collectEvePreflight(request, options) {
|
|
|
1848
2125
|
}
|
|
1849
2126
|
let cloudflare;
|
|
1850
2127
|
let publicOrigin;
|
|
2128
|
+
let priorDeployment;
|
|
1851
2129
|
if (request.command === "deploy") {
|
|
1852
2130
|
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1853
2131
|
try {
|
|
1854
2132
|
cloudflare = await readEveCloudflareTarget({
|
|
1855
2133
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
1856
2134
|
sourceDigest: candidate.sourceDigest,
|
|
2135
|
+
projectRoot: request.projectRoot,
|
|
1857
2136
|
environment: request.environment,
|
|
1858
2137
|
name: request.name,
|
|
1859
2138
|
}, options);
|
|
@@ -1875,6 +2154,17 @@ async function collectEvePreflight(request, options) {
|
|
|
1875
2154
|
};
|
|
1876
2155
|
}
|
|
1877
2156
|
checks.push(...cloudflareChecks(cloudflare));
|
|
2157
|
+
// An owned target also requires its immutable record so the update can
|
|
2158
|
+
// carry the published Durable Object history forward append-only.
|
|
2159
|
+
if (cloudflare.target.state === "owned") {
|
|
2160
|
+
const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, cloudflare.accountId, packaging.project?.projectId === "unknown-project"
|
|
2161
|
+
? undefined
|
|
2162
|
+
: packaging.project?.projectId);
|
|
2163
|
+
priorDeployment = record ?? undefined;
|
|
2164
|
+
if (record === undefined || record.durableObjects === undefined) {
|
|
2165
|
+
checks.push(check("VAL-CLI-007-TARGET-CONFLICT", "failed", "The exact target is Eden-owned, but its immutable deployment record does not carry a readable Durable Object history.", "Re-run the last successful deploy generation or destroy the exact target; updating without the recorded migration history could diverge Durable Object declarations."));
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1878
2168
|
publicOrigin = await resolvedCloudflareOrigin(cloudflare, request.name);
|
|
1879
2169
|
if (publicOrigin === undefined) {
|
|
1880
2170
|
checks.push(check("VAL-HOST-006", "failed", "The authenticated Cloudflare read did not prove the exact workers.dev origin before Eve initialization.", "Return one authenticated account ID, one workers.dev subdomain, and the exact Worker-origin binding."));
|
|
@@ -2006,6 +2296,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2006
2296
|
cloudflare = await readEveCloudflareTarget({
|
|
2007
2297
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
2008
2298
|
sourceDigest: candidate.sourceDigest,
|
|
2299
|
+
projectRoot: request.projectRoot,
|
|
2009
2300
|
environment: request.environment,
|
|
2010
2301
|
name: request.name,
|
|
2011
2302
|
}, options);
|
|
@@ -2049,6 +2340,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2049
2340
|
// reported as EVE_SCHEDULE_UNSUPPORTED and fire only while awake.
|
|
2050
2341
|
schedules: expressible,
|
|
2051
2342
|
cloudflare,
|
|
2343
|
+
...(priorDeployment === undefined ? {} : { priorDeployment }),
|
|
2052
2344
|
...(workflowWorld === undefined ? {} : { workflowWorld }),
|
|
2053
2345
|
};
|
|
2054
2346
|
}
|
|
@@ -2095,7 +2387,7 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
2095
2387
|
message: status === "indeterminate"
|
|
2096
2388
|
? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
|
|
2097
2389
|
: status === "deployed"
|
|
2098
|
-
? "The Eve deployment is healthy and promoted, but exact
|
|
2390
|
+
? "The Eve deployment is healthy and promoted, but exact image cleanup failed; do not retry the deployment."
|
|
2099
2391
|
: "The Eve deployment failed before exact target promotion.",
|
|
2100
2392
|
});
|
|
2101
2393
|
}
|
|
@@ -2223,6 +2515,161 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
2223
2515
|
identity: verified,
|
|
2224
2516
|
};
|
|
2225
2517
|
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Reads the immutable records for this target. The live identity comes from
|
|
2520
|
+
* CURRENT first, then the newest `deployed` record, then the newest record
|
|
2521
|
+
* of any status; the Durable Object history comes from the newest record
|
|
2522
|
+
* that carries one, healthy or not, so a failed update's published tags are
|
|
2523
|
+
* never dropped.
|
|
2524
|
+
*/
|
|
2525
|
+
async function readEveDeploymentRecord(projectRoot, environment, name, accountId, projectId) {
|
|
2526
|
+
const targetsRoot = join(projectRoot, ".eden", "eve-deploy", "targets", `${environment}-${name}`);
|
|
2527
|
+
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2528
|
+
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2529
|
+
const scanned = [];
|
|
2530
|
+
const deploymentIds = new Set();
|
|
2531
|
+
for (const entry of entries) {
|
|
2532
|
+
const generationRoot = join(generationsRoot, entry);
|
|
2533
|
+
const recordPath = join(generationRoot, "deployment.json");
|
|
2534
|
+
const details = await lstat(recordPath).catch(() => undefined);
|
|
2535
|
+
if (details === undefined || !details.isFile())
|
|
2536
|
+
continue;
|
|
2537
|
+
const parsed = await parseEveDeploymentRecord(recordPath, environment, name);
|
|
2538
|
+
if (parsed !== undefined) {
|
|
2539
|
+
// Only records for this account and (when known) this project may
|
|
2540
|
+
// arbitrate ownership or supply migration history.
|
|
2541
|
+
if ((accountId !== undefined && parsed.identity.accountId !== accountId) ||
|
|
2542
|
+
(projectId !== undefined && parsed.identity.projectId !== projectId)) {
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
scanned.push({
|
|
2546
|
+
record: parsed,
|
|
2547
|
+
generationRoot,
|
|
2548
|
+
modifiedAt: details.mtimeMs,
|
|
2549
|
+
});
|
|
2550
|
+
const deploymentId = parsed.deploymentId;
|
|
2551
|
+
if (deploymentId !== undefined)
|
|
2552
|
+
deploymentIds.add(deploymentId);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
scanned.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
2556
|
+
const current = await lstat(join(targetsRoot, "CURRENT")).catch(() => undefined);
|
|
2557
|
+
let live = scanned.find((entry) => entry.record.status === "deployed") ?? scanned[0];
|
|
2558
|
+
if (current !== undefined && current.isSymbolicLink()) {
|
|
2559
|
+
const generationRoot = await realpath(join(targetsRoot, "CURRENT")).catch(() => undefined);
|
|
2560
|
+
if (generationRoot !== undefined) {
|
|
2561
|
+
const pinned = scanned.find((entry) => entry.generationRoot === generationRoot);
|
|
2562
|
+
if (pinned !== undefined) {
|
|
2563
|
+
live = pinned;
|
|
2564
|
+
}
|
|
2565
|
+
else {
|
|
2566
|
+
const parsed = await parseEveDeploymentRecord(join(generationRoot, "deployment.json"), environment, name);
|
|
2567
|
+
if (parsed !== undefined &&
|
|
2568
|
+
!((accountId !== undefined &&
|
|
2569
|
+
parsed.identity.accountId !== accountId) ||
|
|
2570
|
+
(projectId !== undefined &&
|
|
2571
|
+
parsed.identity.projectId !== projectId))) {
|
|
2572
|
+
live = { record: parsed, generationRoot, modifiedAt: 0 };
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
if (live === undefined)
|
|
2578
|
+
return undefined;
|
|
2579
|
+
// Newest record carrying a usable DO history wins, regardless of its
|
|
2580
|
+
// deployment outcome: a tag it published must never be replayed.
|
|
2581
|
+
const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
|
|
2582
|
+
return {
|
|
2583
|
+
recordPath: live.record.recordPath,
|
|
2584
|
+
generationRoot: live.generationRoot,
|
|
2585
|
+
identity: live.record.identity,
|
|
2586
|
+
status: live.record.status,
|
|
2587
|
+
deploymentIds: [...deploymentIds],
|
|
2588
|
+
worldCloudflare: live.record.worldCloudflare,
|
|
2589
|
+
durableObjects: historySource?.record.durableObjects,
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
2593
|
+
const base = await parseEveDestroyRecord(recordPath, environment, name);
|
|
2594
|
+
if (base === undefined)
|
|
2595
|
+
return undefined;
|
|
2596
|
+
let parsed;
|
|
2597
|
+
try {
|
|
2598
|
+
parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
2599
|
+
}
|
|
2600
|
+
catch {
|
|
2601
|
+
return undefined;
|
|
2602
|
+
}
|
|
2603
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2604
|
+
return undefined;
|
|
2605
|
+
const record = parsed;
|
|
2606
|
+
const identityRaw = parsed.identity;
|
|
2607
|
+
const deploymentId = typeof identityRaw === "object" &&
|
|
2608
|
+
identityRaw !== null &&
|
|
2609
|
+
typeof identityRaw
|
|
2610
|
+
.deploymentId === "string"
|
|
2611
|
+
? identityRaw.deploymentId
|
|
2612
|
+
: undefined;
|
|
2613
|
+
const status = typeof record.status === "string" &&
|
|
2614
|
+
["deployed", "failed", "indeterminate"].includes(record.status)
|
|
2615
|
+
? record.status
|
|
2616
|
+
: undefined;
|
|
2617
|
+
const worker = typeof record.worker === "object" && record.worker !== null
|
|
2618
|
+
? record.worker
|
|
2619
|
+
: undefined;
|
|
2620
|
+
const rawBindings = worker !== undefined && "durable_objects" in worker &&
|
|
2621
|
+
typeof worker.durable_objects === "object" &&
|
|
2622
|
+
worker.durable_objects !== null &&
|
|
2623
|
+
"bindings" in worker.durable_objects
|
|
2624
|
+
? worker.durable_objects.bindings
|
|
2625
|
+
: undefined;
|
|
2626
|
+
const rawMigrations = worker !== undefined && "migrations" in worker
|
|
2627
|
+
? worker.migrations
|
|
2628
|
+
: undefined;
|
|
2629
|
+
const rawVars = worker !== undefined && "vars" in worker &&
|
|
2630
|
+
typeof worker.vars === "object" && worker.vars !== null
|
|
2631
|
+
? worker.vars
|
|
2632
|
+
: undefined;
|
|
2633
|
+
const bindings = [];
|
|
2634
|
+
const migrations = [];
|
|
2635
|
+
const durableObjectsValid = Array.isArray(rawBindings) &&
|
|
2636
|
+
Array.isArray(rawMigrations);
|
|
2637
|
+
const invalidHistory = () => ({
|
|
2638
|
+
recordPath,
|
|
2639
|
+
identity: base.identity,
|
|
2640
|
+
status,
|
|
2641
|
+
deploymentId,
|
|
2642
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2643
|
+
durableObjects: undefined,
|
|
2644
|
+
});
|
|
2645
|
+
if (durableObjectsValid) {
|
|
2646
|
+
for (const binding of rawBindings) {
|
|
2647
|
+
if (typeof binding !== "object" || binding === null ||
|
|
2648
|
+
typeof binding.name !== "string" ||
|
|
2649
|
+
typeof binding.class_name !== "string") {
|
|
2650
|
+
return invalidHistory();
|
|
2651
|
+
}
|
|
2652
|
+
bindings.push(binding);
|
|
2653
|
+
}
|
|
2654
|
+
for (const migration of rawMigrations) {
|
|
2655
|
+
if (typeof migration !== "object" || migration === null ||
|
|
2656
|
+
typeof migration.tag !== "string" ||
|
|
2657
|
+
!Array.isArray(migration.new_sqlite_classes) ||
|
|
2658
|
+
(migration.new_sqlite_classes ?? []).some((className) => typeof className !== "string")) {
|
|
2659
|
+
return invalidHistory();
|
|
2660
|
+
}
|
|
2661
|
+
migrations.push(migration);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return {
|
|
2665
|
+
recordPath,
|
|
2666
|
+
identity: base.identity,
|
|
2667
|
+
status,
|
|
2668
|
+
deploymentId,
|
|
2669
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2670
|
+
durableObjects: durableObjectsValid ? { bindings, migrations } : undefined,
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2226
2673
|
async function defaultDestroyTargetRead(request) {
|
|
2227
2674
|
const account = await readWranglerJson(["whoami", "--json"]);
|
|
2228
2675
|
const deployments = await readWranglerJson([
|
|
@@ -2244,18 +2691,25 @@ async function defaultDestroyTargetRead(request) {
|
|
|
2244
2691
|
(exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
|
|
2245
2692
|
const entries = exactContainerEntries(containers.value, request.containerApplicationName);
|
|
2246
2693
|
const firstContainer = entries === undefined ? undefined : entries[0];
|
|
2247
|
-
const
|
|
2248
|
-
let
|
|
2249
|
-
if (
|
|
2694
|
+
const ownedRepositories = request.registryRepositories;
|
|
2695
|
+
let registryTagsPresent;
|
|
2696
|
+
if (ownedRepositories !== undefined && ownedRepositories.length > 0) {
|
|
2250
2697
|
const images = await readWranglerJson([
|
|
2251
2698
|
"containers",
|
|
2252
2699
|
"images",
|
|
2253
2700
|
"list",
|
|
2254
2701
|
"--json",
|
|
2255
2702
|
]);
|
|
2256
|
-
const
|
|
2257
|
-
if (!images.failed &&
|
|
2258
|
-
|
|
2703
|
+
const tagsByRepository = registryTagsByRepository(images.value);
|
|
2704
|
+
if (!images.failed && tagsByRepository !== undefined) {
|
|
2705
|
+
const present = [];
|
|
2706
|
+
for (const [repository, tags] of tagsByRepository) {
|
|
2707
|
+
if (!ownedRepositories.includes(repository))
|
|
2708
|
+
continue;
|
|
2709
|
+
for (const tag of tags)
|
|
2710
|
+
present.push(`${repository}:${tag}`);
|
|
2711
|
+
}
|
|
2712
|
+
registryTagsPresent = present;
|
|
2259
2713
|
}
|
|
2260
2714
|
}
|
|
2261
2715
|
return {
|
|
@@ -2264,21 +2718,22 @@ async function defaultDestroyTargetRead(request) {
|
|
|
2264
2718
|
? {}
|
|
2265
2719
|
: { containerApplicationId: firstContainer.id }),
|
|
2266
2720
|
...(accountId === undefined ? {} : { accountId }),
|
|
2267
|
-
...(
|
|
2721
|
+
...(registryTagsPresent === undefined
|
|
2268
2722
|
? {}
|
|
2269
|
-
: {
|
|
2723
|
+
: { registryTagsPresent }),
|
|
2270
2724
|
};
|
|
2271
2725
|
}
|
|
2272
2726
|
/**
|
|
2273
2727
|
* `wrangler containers images list --json` reports `[{name, tags}]` entries
|
|
2274
|
-
* with the account prefix already stripped from the repository name. The
|
|
2275
|
-
* carries
|
|
2728
|
+
* with the account prefix already stripped from the repository name. The map
|
|
2729
|
+
* carries every tag per repository so owned-repository cleanup can delete
|
|
2730
|
+
* snapshot tags alongside the pushed `candidate` tag.
|
|
2276
2731
|
*/
|
|
2277
|
-
function
|
|
2732
|
+
function registryTagsByRepository(value) {
|
|
2278
2733
|
const collection = jsonCollection(value);
|
|
2279
2734
|
if (collection === undefined)
|
|
2280
2735
|
return undefined;
|
|
2281
|
-
const
|
|
2736
|
+
const tagsByRepository = new Map();
|
|
2282
2737
|
for (const entry of collection) {
|
|
2283
2738
|
if (typeof entry !== "object" || entry === null || !("name" in entry)) {
|
|
2284
2739
|
return undefined;
|
|
@@ -2287,13 +2742,15 @@ function registryImageNames(value) {
|
|
|
2287
2742
|
const tags = "tags" in entry ? entry.tags : undefined;
|
|
2288
2743
|
if (typeof name !== "string" || !Array.isArray(tags))
|
|
2289
2744
|
return undefined;
|
|
2745
|
+
const validTags = [];
|
|
2290
2746
|
for (const tag of tags) {
|
|
2291
2747
|
if (typeof tag !== "string")
|
|
2292
2748
|
return undefined;
|
|
2293
|
-
|
|
2749
|
+
validTags.push(tag);
|
|
2294
2750
|
}
|
|
2751
|
+
tagsByRepository.set(name, validTags);
|
|
2295
2752
|
}
|
|
2296
|
-
return
|
|
2753
|
+
return tagsByRepository;
|
|
2297
2754
|
}
|
|
2298
2755
|
async function defaultWorkerDelete(request) {
|
|
2299
2756
|
const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
|
|
@@ -2313,23 +2770,59 @@ async function defaultContainerDelete(request) {
|
|
|
2313
2770
|
}
|
|
2314
2771
|
return "indeterminate";
|
|
2315
2772
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2773
|
+
/**
|
|
2774
|
+
* Deletes every tag in an owned registry repository — the `candidate` tag
|
|
2775
|
+
* Eden pushed plus any `rootfs-snapshot-*`/`rootfs-set-*` tags container
|
|
2776
|
+
* snapshots created under the source image repository. Deleting only
|
|
2777
|
+
* `candidate` would leave snapshot storage behind, so each tag in the
|
|
2778
|
+
* repository is deleted individually.
|
|
2779
|
+
*/
|
|
2780
|
+
async function defaultRegistryRepositoryDelete(request) {
|
|
2781
|
+
const images = await readWranglerJson([
|
|
2782
|
+
"containers",
|
|
2783
|
+
"images",
|
|
2784
|
+
"list",
|
|
2785
|
+
"--json",
|
|
2786
|
+
]);
|
|
2787
|
+
const collection = jsonCollection(images.value);
|
|
2788
|
+
if (images.failed || collection === undefined)
|
|
2789
|
+
return "indeterminate";
|
|
2790
|
+
const entry = collection.find((candidate) => typeof candidate === "object" &&
|
|
2791
|
+
candidate !== null &&
|
|
2792
|
+
candidate.name === request.repository);
|
|
2793
|
+
if (entry === undefined)
|
|
2321
2794
|
return "absent";
|
|
2795
|
+
const tags = entry.tags;
|
|
2796
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) {
|
|
2797
|
+
return "indeterminate";
|
|
2322
2798
|
}
|
|
2323
|
-
|
|
2799
|
+
let deletedAny = false;
|
|
2800
|
+
for (const tag of tags) {
|
|
2801
|
+
const result = await runWranglerWithInput([
|
|
2802
|
+
"containers",
|
|
2803
|
+
"images",
|
|
2804
|
+
"delete",
|
|
2805
|
+
`${request.repository}:${tag}`,
|
|
2806
|
+
"--skip-confirmation",
|
|
2807
|
+
], process.cwd());
|
|
2808
|
+
if (result.exitCode === 0) {
|
|
2809
|
+
deletedAny = true;
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
|
|
2813
|
+
return "indeterminate";
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return deletedAny ? "deleted" : "absent";
|
|
2324
2817
|
}
|
|
2325
2818
|
/**
|
|
2326
|
-
* The
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2819
|
+
* The registry repository this generation's deployment record proves Eden
|
|
2820
|
+
* pushed to, or undefined. Ownership holds only when the recorded digest
|
|
2821
|
+
* reference points at the repository derived from the same identity that
|
|
2822
|
+
* produced it; a digest-bearing reference to any other repository was
|
|
2823
|
+
* published outside Eden's push and is never owned here.
|
|
2331
2824
|
*/
|
|
2332
|
-
function
|
|
2825
|
+
function ownedRegistryRepository(identity, environment, name, accountId) {
|
|
2333
2826
|
if (identity.projectId === undefined ||
|
|
2334
2827
|
identity.generationId === undefined ||
|
|
2335
2828
|
identity.containerImage === undefined ||
|
|
@@ -2342,14 +2835,14 @@ function ownedRegistryImageRef(identity, environment, name, accountId) {
|
|
|
2342
2835
|
imageDigestFromReference(identity.containerImage) === undefined) {
|
|
2343
2836
|
return undefined;
|
|
2344
2837
|
}
|
|
2345
|
-
return
|
|
2838
|
+
return repository;
|
|
2346
2839
|
}
|
|
2347
2840
|
/**
|
|
2348
|
-
* The
|
|
2349
|
-
* retained attempt evidence names the target's repository; the account
|
|
2350
|
-
* target key must match the verified destroy record.
|
|
2841
|
+
* The registry repository an indeterminate deploy attempt provably pushed.
|
|
2842
|
+
* The retained attempt evidence names the target's repository; the account
|
|
2843
|
+
* and target key must match the verified destroy record.
|
|
2351
2844
|
*/
|
|
2352
|
-
async function
|
|
2845
|
+
async function attemptRegistryRepository(attemptPath, accountId, targetKey) {
|
|
2353
2846
|
const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
|
|
2354
2847
|
if (raw === undefined)
|
|
2355
2848
|
return undefined;
|
|
@@ -2369,7 +2862,7 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
|
|
|
2369
2862
|
(!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
|
|
2370
2863
|
"accountId" in parsed &&
|
|
2371
2864
|
parsed.accountId === accountId) {
|
|
2372
|
-
return
|
|
2865
|
+
return parsed.imageRepository;
|
|
2373
2866
|
}
|
|
2374
2867
|
if ("imageReference" in parsed &&
|
|
2375
2868
|
typeof parsed.imageReference === "string" &&
|
|
@@ -2379,39 +2872,40 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
|
|
|
2379
2872
|
if (repository !== undefined &&
|
|
2380
2873
|
repository.startsWith(expected) &&
|
|
2381
2874
|
(!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
|
|
2382
|
-
return
|
|
2875
|
+
return repository.slice(`registry.cloudflare.com/${accountId}/`.length);
|
|
2383
2876
|
}
|
|
2384
2877
|
}
|
|
2385
2878
|
return undefined;
|
|
2386
2879
|
}
|
|
2387
2880
|
/**
|
|
2388
|
-
* Every registry
|
|
2881
|
+
* Every registry repository this exact target provably owns: the recorded
|
|
2389
2882
|
* digest references in matching deployment records plus the repository
|
|
2390
2883
|
* evidence retained by indeterminate pushes. Never derived by prefix-matching
|
|
2391
|
-
* the registry; each
|
|
2884
|
+
* the registry; each repository is computed from the immutable generation
|
|
2885
|
+
* records.
|
|
2392
2886
|
*/
|
|
2393
|
-
async function
|
|
2887
|
+
async function collectEveDestroyRegistryRepositories(projectRoot, environment, name, record) {
|
|
2394
2888
|
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2395
2889
|
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2396
|
-
const
|
|
2890
|
+
const repositories = new Set();
|
|
2397
2891
|
const targetKey = record.projectId === undefined
|
|
2398
2892
|
? undefined
|
|
2399
2893
|
: stableDeploymentKey(record.projectId, record.accountId, environment, name);
|
|
2400
2894
|
for (const entry of entries) {
|
|
2401
2895
|
const generationRoot = join(generationsRoot, entry);
|
|
2402
2896
|
const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
|
|
2403
|
-
const
|
|
2897
|
+
const deployedRepository = parsed === undefined
|
|
2404
2898
|
? undefined
|
|
2405
|
-
:
|
|
2406
|
-
if (
|
|
2407
|
-
|
|
2899
|
+
: ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
|
|
2900
|
+
if (deployedRepository !== undefined)
|
|
2901
|
+
repositories.add(deployedRepository);
|
|
2408
2902
|
if (targetKey === undefined)
|
|
2409
2903
|
continue;
|
|
2410
|
-
const
|
|
2411
|
-
if (
|
|
2412
|
-
|
|
2904
|
+
const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
|
|
2905
|
+
if (attemptRepository !== undefined)
|
|
2906
|
+
repositories.add(attemptRepository);
|
|
2413
2907
|
}
|
|
2414
|
-
return [...
|
|
2908
|
+
return [...repositories];
|
|
2415
2909
|
}
|
|
2416
2910
|
export async function runEveDestroy(request, options = {}) {
|
|
2417
2911
|
const startedAt = Date.now();
|
|
@@ -2464,11 +2958,11 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2464
2958
|
}
|
|
2465
2959
|
checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
|
|
2466
2960
|
eveProgress(options, "verifying the immutable ownership record");
|
|
2467
|
-
const
|
|
2961
|
+
const ownedRegistryRepositories = await collectEveDestroyRegistryRepositories(request.projectRoot, request.environment, request.name, record.identity);
|
|
2468
2962
|
const readTarget = () => Promise.resolve(reader({
|
|
2469
2963
|
workerName: expectedWorker,
|
|
2470
2964
|
containerApplicationName: expectedContainer,
|
|
2471
|
-
|
|
2965
|
+
registryRepositories: ownedRegistryRepositories,
|
|
2472
2966
|
}));
|
|
2473
2967
|
const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
|
|
2474
2968
|
eveProgress(options, "reading the exact remote inventory");
|
|
@@ -2478,9 +2972,9 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2478
2972
|
}
|
|
2479
2973
|
if (!before.workerExists &&
|
|
2480
2974
|
before.containerApplicationId === undefined &&
|
|
2481
|
-
(
|
|
2482
|
-
(before.
|
|
2483
|
-
before.
|
|
2975
|
+
(ownedRegistryRepositories.length === 0 ||
|
|
2976
|
+
(before.registryTagsPresent !== undefined &&
|
|
2977
|
+
before.registryTagsPresent.length === 0))) {
|
|
2484
2978
|
emit({
|
|
2485
2979
|
ok: true,
|
|
2486
2980
|
status: "absent",
|
|
@@ -2537,17 +3031,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2537
3031
|
? "The exact owned Container application was deleted."
|
|
2538
3032
|
: "The exact Container application was already absent."));
|
|
2539
3033
|
}
|
|
2540
|
-
const
|
|
2541
|
-
if (
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
3034
|
+
const repositoryDeletes = new Map();
|
|
3035
|
+
if (ownedRegistryRepositories.length > 0) {
|
|
3036
|
+
const deleteRegistryRepository = options.deleteRegistryRepository ?? defaultRegistryRepositoryDelete;
|
|
3037
|
+
// Every tag inside an owned repository is deleted (candidate plus
|
|
3038
|
+
// snapshot tags). When the registry could not be enumerated, still
|
|
3039
|
+
// attempt the exact owned repositories; absence verification below
|
|
3040
|
+
// re-reads the registry when it becomes readable.
|
|
3041
|
+
const presentRepositories = new Set((before.registryTagsPresent ?? []).map((ref) => ref.split(":")[0] ?? ""));
|
|
3042
|
+
const deletable = before.registryTagsPresent === undefined
|
|
3043
|
+
? ownedRegistryRepositories
|
|
3044
|
+
: ownedRegistryRepositories.filter((repository) => presentRepositories.has(repository));
|
|
2546
3045
|
if (deletable.length > 0) {
|
|
2547
|
-
eveProgress(options, `deleting ${deletable.length} registry
|
|
3046
|
+
eveProgress(options, `deleting ${deletable.length} registry repositor${deletable.length === 1 ? "y" : "ies"}`);
|
|
2548
3047
|
}
|
|
2549
|
-
for (const
|
|
2550
|
-
|
|
3048
|
+
for (const repository of deletable) {
|
|
3049
|
+
repositoryDeletes.set(repository, await deleteRegistryRepository({ repository }));
|
|
2551
3050
|
}
|
|
2552
3051
|
}
|
|
2553
3052
|
eveProgress(options, "verifying the exact target is absent");
|
|
@@ -2565,15 +3064,16 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2565
3064
|
});
|
|
2566
3065
|
fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
|
|
2567
3066
|
}
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
3067
|
+
const presentTagsAfter = after.registryTagsPresent ?? [];
|
|
3068
|
+
const retainedRepositories = ownedRegistryRepositories.filter((repository) => presentTagsAfter.some((ref) => ref.startsWith(`${repository}:`)) ||
|
|
3069
|
+
(after.registryTagsPresent === undefined &&
|
|
3070
|
+
repositoryDeletes.get(repository) === "indeterminate"));
|
|
3071
|
+
if (ownedRegistryRepositories.length > 0) {
|
|
3072
|
+
checks.push(retainedRepositories.length === 0
|
|
2573
3073
|
? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
|
|
2574
|
-
: check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${
|
|
3074
|
+
: check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image tag(s) remain after destroy: ${retainedRepositories.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed repositories' tags with wrangler containers images delete."));
|
|
2575
3075
|
}
|
|
2576
|
-
if (
|
|
3076
|
+
if (retainedRepositories.length > 0) {
|
|
2577
3077
|
emit({
|
|
2578
3078
|
ok: false,
|
|
2579
3079
|
status: "failed",
|
|
@@ -2581,7 +3081,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2581
3081
|
name: request.name,
|
|
2582
3082
|
checks,
|
|
2583
3083
|
});
|
|
2584
|
-
fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${
|
|
3084
|
+
fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image tag(s) remain in: ${retainedRepositories.join(", ")}.`);
|
|
2585
3085
|
}
|
|
2586
3086
|
checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
|
|
2587
3087
|
const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
|