@moinulmoin/eden 0.4.1 → 0.5.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/README.md +3 -3
- package/dist/eve-control-plane.d.ts +38 -1
- package/dist/eve-control-plane.js +466 -40
- package/dist/eve.js +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,15 +12,15 @@ If you already have an Eve project, start with `eden deploy`.
|
|
|
12
12
|
Use any one of these package managers:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
npm install --global @moinulmoin/eden@0.
|
|
15
|
+
npm install --global @moinulmoin/eden@0.5.0
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
pnpm add --global @moinulmoin/eden@0.
|
|
19
|
+
pnpm add --global @moinulmoin/eden@0.5.0
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
|
-
bun add --global @moinulmoin/eden@0.
|
|
23
|
+
bun add --global @moinulmoin/eden@0.5.0
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Then confirm the CLI is available:
|
|
@@ -21,6 +21,12 @@ export interface EvePreflightCandidate {
|
|
|
21
21
|
export type EveDeploymentStatus = "deployed" | "failed" | "indeterminate";
|
|
22
22
|
export interface EveDeploymentMetadata {
|
|
23
23
|
readonly status: EveDeploymentStatus;
|
|
24
|
+
/**
|
|
25
|
+
* `create` is a first deploy onto an absent target; `update` is an
|
|
26
|
+
* in-place redeploy of a provably Eden-owned target that keeps the Worker,
|
|
27
|
+
* Durable Object classes, and durable state.
|
|
28
|
+
*/
|
|
29
|
+
readonly operation: "create" | "update";
|
|
24
30
|
readonly targetKey: string;
|
|
25
31
|
readonly deploymentId: string;
|
|
26
32
|
readonly workerName: string;
|
|
@@ -28,6 +34,9 @@ export interface EveDeploymentMetadata {
|
|
|
28
34
|
readonly stableContainerInstanceName: string;
|
|
29
35
|
readonly stableWorkersDevOrigin: string;
|
|
30
36
|
readonly imageReference: string;
|
|
37
|
+
/** Present on updates: the superseded generation and its registry image. */
|
|
38
|
+
readonly supersededGenerationId?: string;
|
|
39
|
+
readonly supersededImageReference?: string;
|
|
31
40
|
readonly runtimeVariableNames: readonly string[];
|
|
32
41
|
readonly evidenceRetained: true;
|
|
33
42
|
}
|
|
@@ -50,6 +59,7 @@ export type EveCloudflareTargetState = "absent" | "owned" | "unowned" | "mismatc
|
|
|
50
59
|
export interface EveCloudflareReadRequest {
|
|
51
60
|
readonly projectId: string;
|
|
52
61
|
readonly sourceDigest: string;
|
|
62
|
+
readonly projectRoot: string;
|
|
53
63
|
readonly environment: EveCliEnvironment;
|
|
54
64
|
readonly name: string;
|
|
55
65
|
}
|
|
@@ -65,6 +75,29 @@ export interface EveCloudflareReadResult {
|
|
|
65
75
|
readonly message?: string;
|
|
66
76
|
readonly remediation?: string;
|
|
67
77
|
readonly identity?: EveDeploymentIdentityProof;
|
|
78
|
+
/**
|
|
79
|
+
* Which exact resources the read observed. `undefined` means the
|
|
80
|
+
* inventory could not be proven; `worker`/`container` booleans mean the
|
|
81
|
+
* exact names were present or absent without any ownership claim.
|
|
82
|
+
*/
|
|
83
|
+
readonly observed?: {
|
|
84
|
+
readonly worker: boolean;
|
|
85
|
+
readonly container: boolean;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Provider-backed facts about the live target, read through the
|
|
89
|
+
* Cloudflare API (not self-asserted response headers, which any Worker
|
|
90
|
+
* can forge): the Container application's configured image and the
|
|
91
|
+
* `EDEN_EVE_DEPLOYMENT_ID` plain-text var Eden stamps into every Worker
|
|
92
|
+
* it publishes. `settingsReadable` tells the ownership resolver whether
|
|
93
|
+
* a missing var is evidence of a foreign Worker or just an unreadable
|
|
94
|
+
* settings call.
|
|
95
|
+
*/
|
|
96
|
+
readonly providerEvidence?: {
|
|
97
|
+
readonly containerImage?: string;
|
|
98
|
+
readonly workerDeploymentId?: string;
|
|
99
|
+
readonly settingsReadable?: boolean;
|
|
100
|
+
};
|
|
68
101
|
};
|
|
69
102
|
}
|
|
70
103
|
export type EveCloudflareReadRunner = (request: EveCloudflareReadRequest) => EveCloudflareReadResult | Promise<EveCloudflareReadResult>;
|
|
@@ -104,7 +137,10 @@ export interface EveDeploymentIdentity {
|
|
|
104
137
|
readonly runtimeVariableNames: readonly string[];
|
|
105
138
|
readonly runtimeRevisionHandle?: string;
|
|
106
139
|
}
|
|
107
|
-
export type EveDeploymentIdentityProof = Partial<EveDeploymentIdentity
|
|
140
|
+
export type EveDeploymentIdentityProof = Partial<EveDeploymentIdentity> & {
|
|
141
|
+
/** The deployment the serving Container verifiably started under; set only by a deployed Eden host that proves its running image before stamping. */
|
|
142
|
+
readonly startedDeploymentId?: string;
|
|
143
|
+
};
|
|
108
144
|
export interface EveDeploymentPublicationRequest {
|
|
109
145
|
readonly identity: EveDeploymentIdentity;
|
|
110
146
|
readonly hostConfig: EveHostConfig;
|
|
@@ -252,6 +288,7 @@ export interface EveDestroyCloudflareReadRequest {
|
|
|
252
288
|
export declare function exactTargetContainerEntries(value: unknown, name: string): readonly {
|
|
253
289
|
readonly id: string;
|
|
254
290
|
readonly name: string;
|
|
291
|
+
readonly image?: string;
|
|
255
292
|
}[] | undefined;
|
|
256
293
|
export declare function runEveControlPlane(request: EveCliExecutionRequest, options?: EvePreflightOptions): Promise<void>;
|
|
257
294
|
export interface EveDestroyOutcome {
|
|
@@ -290,7 +290,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
290
290
|
if (result.ok) {
|
|
291
291
|
if (result.command === "eve deploy") {
|
|
292
292
|
const url = result.deployment?.stableWorkersDevOrigin;
|
|
293
|
-
lines.push("", `✓ deployed ${result.name} (${result.environment})` +
|
|
293
|
+
lines.push("", `✓ ${result.deployment?.operation === "update" ? "updated" : "deployed"} ${result.name} (${result.environment})` +
|
|
294
294
|
(eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
|
|
295
295
|
if (url !== undefined)
|
|
296
296
|
lines.push(url);
|
|
@@ -306,7 +306,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
306
306
|
lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
|
|
307
307
|
? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
|
|
308
308
|
: result.deployment.status === "deployed"
|
|
309
|
-
? "The deployment is live; only local runtime
|
|
309
|
+
? "The deployment is live; only image cleanup failed (local runtime image or the superseded registry tag), so do not retry the deployment."
|
|
310
310
|
: "The deployment failed before promotion; the exact target was not claimed."}`);
|
|
311
311
|
}
|
|
312
312
|
else if (firstFailure !== undefined) {
|
|
@@ -442,8 +442,21 @@ function exactContainerEntries(value, name) {
|
|
|
442
442
|
}
|
|
443
443
|
const id = entry.id;
|
|
444
444
|
const entryName = entry.name;
|
|
445
|
-
if (entryName === name)
|
|
446
|
-
|
|
445
|
+
if (entryName === name) {
|
|
446
|
+
const configuration = entry.configuration;
|
|
447
|
+
const image = typeof entry.image === "string"
|
|
448
|
+
? entry.image
|
|
449
|
+
: typeof configuration === "object" &&
|
|
450
|
+
configuration !== null &&
|
|
451
|
+
typeof configuration.image === "string"
|
|
452
|
+
? configuration.image
|
|
453
|
+
: undefined;
|
|
454
|
+
entries.push({
|
|
455
|
+
id,
|
|
456
|
+
name: entryName,
|
|
457
|
+
...(image === undefined ? {} : { image }),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
447
460
|
}
|
|
448
461
|
if (entries.length === 0 && collection.length >= 100) {
|
|
449
462
|
return undefined;
|
|
@@ -595,6 +608,9 @@ async function defaultCloudflareRead(request) {
|
|
|
595
608
|
target: { state: "absent" },
|
|
596
609
|
};
|
|
597
610
|
}
|
|
611
|
+
const workerPresent = deploymentEntries.length > 0;
|
|
612
|
+
const containerPresent = (containerEntries?.length ?? 0) > 0;
|
|
613
|
+
const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0]?.image : undefined);
|
|
598
614
|
return {
|
|
599
615
|
accountAccess,
|
|
600
616
|
containerAccess,
|
|
@@ -605,16 +621,141 @@ async function defaultCloudflareRead(request) {
|
|
|
605
621
|
? {}
|
|
606
622
|
: { workersDevSubdomain }),
|
|
607
623
|
target: {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
624
|
+
// The read-only inventory proves the exact target exists but cannot
|
|
625
|
+
// prove who created it; Eden's immutable deployment record resolves
|
|
626
|
+
// ownership next.
|
|
627
|
+
state: "unowned",
|
|
628
|
+
observed: { worker: workerPresent, container: containerPresent },
|
|
629
|
+
...(providerEvidence === undefined ? {} : { providerEvidence }),
|
|
630
|
+
message: "The exact Worker or Container target already exists, and the read-only inventory cannot prove Eden owns it.",
|
|
631
|
+
remediation: "Deploy under a different --name or remove the conflicting Worker/Container yourself; Eden never adopts or replaces resources it cannot prove it created.",
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Reads provider-backed facts about the live target: the Container
|
|
637
|
+
* application's configured image (from `wrangler containers list`) and the
|
|
638
|
+
* `EDEN_EVE_DEPLOYMENT_ID` plain-text var Eden stamps into every Worker it
|
|
639
|
+
* publishes (from the script settings API). Both are provider state — a
|
|
640
|
+
* foreign Worker can forge `x-eden-*` response headers but cannot set a var
|
|
641
|
+
* Eden recorded at publish time.
|
|
642
|
+
*/
|
|
643
|
+
async function readProviderEvidence(workerName, accountId, workerPresent, containerImage) {
|
|
644
|
+
const evidence = {
|
|
645
|
+
...(containerImage === undefined ? {} : { containerImage }),
|
|
646
|
+
};
|
|
647
|
+
if (workerPresent && accountId !== undefined) {
|
|
648
|
+
const token = process.env.CLOUDFLARE_API_TOKEN ??
|
|
649
|
+
(await readWranglerOAuthToken());
|
|
650
|
+
if (token !== undefined) {
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${encodeURIComponent(workerName)}/settings`, {
|
|
653
|
+
headers: { authorization: `Bearer ${token}` },
|
|
654
|
+
signal: AbortSignal.timeout(20_000),
|
|
655
|
+
});
|
|
656
|
+
if (response.ok) {
|
|
657
|
+
evidence.settingsReadable = true;
|
|
658
|
+
const parsed = (await response.json());
|
|
659
|
+
const binding = parsed.result?.bindings?.find((value) => value.type === "plain_text" &&
|
|
660
|
+
value.name === "EDEN_EVE_DEPLOYMENT_ID");
|
|
661
|
+
if (typeof binding?.text === "string") {
|
|
662
|
+
evidence.workerDeploymentId = binding.text;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else if (response.status !== 404) {
|
|
666
|
+
evidence.settingsReadable = false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
evidence.settingsReadable = false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return evidence;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Resolves `unowned` reads against Eden's immutable deployment records AND
|
|
678
|
+
* the live remote target. The local record proves Eden once created the
|
|
679
|
+
* target; the remote Worker's stamped identity proves it is still the one
|
|
680
|
+
* Eden deployed — a same-name Worker recreated by something else after the
|
|
681
|
+
* target was destroyed (or a forged local record) must never be upgraded to
|
|
682
|
+
* `owned`.
|
|
683
|
+
*/
|
|
684
|
+
async function resolveEveTargetOwnership(request, result) {
|
|
685
|
+
if (result.target.state !== "unowned")
|
|
686
|
+
return result;
|
|
687
|
+
const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, result.accountId, request.projectId === "unknown-project" ? undefined : request.projectId);
|
|
688
|
+
if (record === undefined ||
|
|
689
|
+
record.identity.workerName !== request.name ||
|
|
690
|
+
record.identity.accountId !== result.accountId ||
|
|
691
|
+
(request.projectId !== "unknown-project" &&
|
|
692
|
+
record.identity.projectId !== undefined &&
|
|
693
|
+
record.identity.projectId !== request.projectId) ||
|
|
694
|
+
(result.target.observed?.container === true &&
|
|
695
|
+
record.identity.containerApplicationName !==
|
|
696
|
+
boundedResourceName(request.name, "container"))) {
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
// Remote proof must come from provider state. A foreign Worker can echo
|
|
700
|
+
// `x-eden-*` response headers, but it cannot plant the plain-text var Eden
|
|
701
|
+
// publishes into script settings, nor point the Container application at an
|
|
702
|
+
// image digest Eden recorded. The Worker is proven only by its own settings:
|
|
703
|
+
// a matching Container image proves the Container, never the Worker, so
|
|
704
|
+
// unreadable settings fail closed.
|
|
705
|
+
const evidence = result.target.providerEvidence;
|
|
706
|
+
const recordedDeploymentIds = new Set(record.deploymentIds);
|
|
707
|
+
const workerProven = result.target.observed?.worker !== true ||
|
|
708
|
+
(evidence?.settingsReadable === true &&
|
|
709
|
+
evidence.workerDeploymentId !== undefined &&
|
|
710
|
+
recordedDeploymentIds.has(evidence.workerDeploymentId));
|
|
711
|
+
const containerProven = result.target.observed?.container !== true ||
|
|
712
|
+
(evidence?.containerImage !== undefined &&
|
|
713
|
+
record.images.includes(evidence.containerImage));
|
|
714
|
+
const remoteProven = evidence !== undefined && workerProven && containerProven;
|
|
715
|
+
if (!remoteProven) {
|
|
716
|
+
return {
|
|
717
|
+
...result,
|
|
718
|
+
target: {
|
|
719
|
+
...result.target,
|
|
720
|
+
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.",
|
|
721
|
+
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.",
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const priorIdentity = {
|
|
726
|
+
workerName: record.identity.workerName,
|
|
727
|
+
containerApplicationName: record.identity.containerApplicationName,
|
|
728
|
+
stableContainerInstanceName: record.identity.stableContainerInstanceName,
|
|
729
|
+
accountId: record.identity.accountId,
|
|
730
|
+
environment: request.environment,
|
|
731
|
+
name: request.name,
|
|
732
|
+
...(record.identity.projectId === undefined
|
|
733
|
+
? {}
|
|
734
|
+
: { projectId: record.identity.projectId }),
|
|
735
|
+
...(record.identity.generationId === undefined
|
|
736
|
+
? {}
|
|
737
|
+
: { generationId: record.identity.generationId }),
|
|
738
|
+
...(record.identity.containerImage === undefined
|
|
739
|
+
? {}
|
|
740
|
+
: { containerImage: record.identity.containerImage }),
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
...result,
|
|
744
|
+
target: {
|
|
745
|
+
state: "owned",
|
|
746
|
+
identity: priorIdentity,
|
|
747
|
+
...(result.target.observed === undefined
|
|
748
|
+
? {}
|
|
749
|
+
: { observed: result.target.observed }),
|
|
750
|
+
message: "The immutable deployment record proves Eden owns the exact target; deploy proceeds as an in-place update.",
|
|
611
751
|
},
|
|
612
752
|
};
|
|
613
753
|
}
|
|
614
754
|
async function readEveCloudflareTarget(request, options) {
|
|
615
|
-
|
|
616
|
-
? defaultCloudflareRead(request)
|
|
617
|
-
: options.cloudflareRead(request);
|
|
755
|
+
const result = options.cloudflareRead === undefined
|
|
756
|
+
? await defaultCloudflareRead(request)
|
|
757
|
+
: await options.cloudflareRead(request);
|
|
758
|
+
return resolveEveTargetOwnership(request, result);
|
|
618
759
|
}
|
|
619
760
|
async function resolvedCloudflareOrigin(result, workerName) {
|
|
620
761
|
if (result.accountId === undefined ||
|
|
@@ -713,12 +854,12 @@ function cloudflareChecks(result) {
|
|
|
713
854
|
? undefined
|
|
714
855
|
: "Use an authenticated read-only adapter with account and Container access; preflight never uploads, publishes, or changes remote state.";
|
|
715
856
|
const target = result.target;
|
|
716
|
-
const targetPasses = target.state === "absent" ||
|
|
717
|
-
(target.state === "owned" && target.matchesCandidate === true);
|
|
857
|
+
const targetPasses = target.state === "absent" || target.state === "owned";
|
|
718
858
|
const targetMessage = targetPasses
|
|
719
859
|
? target.state === "absent"
|
|
720
860
|
? "The exact target read is absent and is eligible without claiming ownership."
|
|
721
|
-
:
|
|
861
|
+
: target.message ??
|
|
862
|
+
"The exact target is provably Eden-owned; deploy proceeds as an in-place update."
|
|
722
863
|
: target.message ??
|
|
723
864
|
"The exact target read is unowned, mismatched, cross-environment, or ambiguous.";
|
|
724
865
|
const targetRemediation = targetPasses
|
|
@@ -1062,6 +1203,10 @@ function responseIdentity(response, body) {
|
|
|
1062
1203
|
read("workerName", "x-eden-eve-worker-name");
|
|
1063
1204
|
read("containerApplicationName", "x-eden-eve-container-application");
|
|
1064
1205
|
read("stableContainerInstanceName", "x-eden-eve-container-instance");
|
|
1206
|
+
const started = response.headers.get("x-eden-eve-started-deployment");
|
|
1207
|
+
if (started !== null) {
|
|
1208
|
+
identity["startedDeploymentId"] = started;
|
|
1209
|
+
}
|
|
1065
1210
|
read("containerImage", "x-eden-eve-container-image");
|
|
1066
1211
|
return identity;
|
|
1067
1212
|
}
|
|
@@ -1105,9 +1250,20 @@ function defaultHealthRunner() {
|
|
|
1105
1250
|
body.state === "ready" ||
|
|
1106
1251
|
body.ready === true);
|
|
1107
1252
|
if (ready) {
|
|
1253
|
+
const observedIdentity = responseIdentity(response, body);
|
|
1254
|
+
// A Worker activates before its image rollout completes; promotion
|
|
1255
|
+
// must wait until the *served container* verifiably runs this
|
|
1256
|
+
// generation's image (the host stamps the header only after
|
|
1257
|
+
// inspecting the running instance).
|
|
1258
|
+
if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
|
|
1259
|
+
lastReason =
|
|
1260
|
+
"The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
|
|
1261
|
+
await wait();
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1108
1264
|
return {
|
|
1109
1265
|
status: "ready",
|
|
1110
|
-
identity:
|
|
1266
|
+
identity: observedIdentity,
|
|
1111
1267
|
};
|
|
1112
1268
|
}
|
|
1113
1269
|
lastReason =
|
|
@@ -1161,8 +1317,13 @@ async function releaseEveTargetLock(lock) {
|
|
|
1161
1317
|
await rm(lock.path, { force: true });
|
|
1162
1318
|
return (await lstat(lock.path).catch(() => undefined)) === undefined;
|
|
1163
1319
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1320
|
+
/**
|
|
1321
|
+
* Atomically re-points the target's CURRENT symlink at a promoted immutable
|
|
1322
|
+
* generation. The pointer key is `<environment>-<name>` — the same path
|
|
1323
|
+
* `destroy` clears and the update ownership resolver reads.
|
|
1324
|
+
*/
|
|
1325
|
+
async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoot) {
|
|
1326
|
+
const targetRoot = join(deploymentRoot, "targets", pointerKey);
|
|
1166
1327
|
const artifactBoundary = await realpath(dirname(deploymentRoot)).catch(() => undefined);
|
|
1167
1328
|
const generationCanonical = await realpath(generationRoot).catch(() => undefined);
|
|
1168
1329
|
if (artifactBoundary === undefined ||
|
|
@@ -1191,7 +1352,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1191
1352
|
throw new EveCliError({
|
|
1192
1353
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1193
1354
|
message: "The Eve target pointer directory could not be verified safely.",
|
|
1194
|
-
source:
|
|
1355
|
+
source: pointerKey,
|
|
1195
1356
|
});
|
|
1196
1357
|
}
|
|
1197
1358
|
const currentPath = join(targetRoot, "CURRENT");
|
|
@@ -1208,7 +1369,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1208
1369
|
throw new EveCliError({
|
|
1209
1370
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1210
1371
|
message: "The Eve target pointer did not resolve to the promoted generation.",
|
|
1211
|
-
source:
|
|
1372
|
+
source: pointerKey,
|
|
1212
1373
|
});
|
|
1213
1374
|
}
|
|
1214
1375
|
return currentPath;
|
|
@@ -1242,9 +1403,10 @@ async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
|
|
|
1242
1403
|
flag: "wx",
|
|
1243
1404
|
});
|
|
1244
1405
|
}
|
|
1245
|
-
function deploymentMetadata(identity, targetKey, status) {
|
|
1406
|
+
function deploymentMetadata(identity, targetKey, status, operation, superseded) {
|
|
1246
1407
|
return {
|
|
1247
1408
|
status,
|
|
1409
|
+
operation,
|
|
1248
1410
|
targetKey,
|
|
1249
1411
|
deploymentId: identity.deploymentId,
|
|
1250
1412
|
workerName: identity.workerName,
|
|
@@ -1252,6 +1414,12 @@ function deploymentMetadata(identity, targetKey, status) {
|
|
|
1252
1414
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1253
1415
|
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1254
1416
|
imageReference: identity.containerImage,
|
|
1417
|
+
...(superseded?.generationId === undefined
|
|
1418
|
+
? {}
|
|
1419
|
+
: { supersededGenerationId: superseded.generationId }),
|
|
1420
|
+
...(superseded?.containerImage === undefined
|
|
1421
|
+
? {}
|
|
1422
|
+
: { supersededImageReference: superseded.containerImage }),
|
|
1255
1423
|
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1256
1424
|
evidenceRetained: true,
|
|
1257
1425
|
};
|
|
@@ -1273,6 +1441,46 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1273
1441
|
collected.projectId === undefined) {
|
|
1274
1442
|
throw deploymentFailure("EVE_DEPLOY_CHECKS_FAILED", "The inline Eve deploy checks did not return a complete immutable candidate.");
|
|
1275
1443
|
}
|
|
1444
|
+
const isUpdate = cloudflare.target.state === "owned";
|
|
1445
|
+
const operation = isUpdate ? "update" : "create";
|
|
1446
|
+
const prior = collected.priorDeployment;
|
|
1447
|
+
if (isUpdate && prior === undefined) {
|
|
1448
|
+
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.");
|
|
1449
|
+
}
|
|
1450
|
+
if (isUpdate && prior?.durableObjects === undefined) {
|
|
1451
|
+
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.");
|
|
1452
|
+
}
|
|
1453
|
+
const priorIdentity = prior?.identity;
|
|
1454
|
+
const priorDurableObjects = prior?.durableObjects;
|
|
1455
|
+
const superseded = isUpdate
|
|
1456
|
+
? {
|
|
1457
|
+
generationId: priorIdentity?.generationId,
|
|
1458
|
+
containerImage: priorIdentity?.containerImage,
|
|
1459
|
+
}
|
|
1460
|
+
: undefined;
|
|
1461
|
+
const resultWarnings = collected.result.warnings;
|
|
1462
|
+
if (isUpdate) {
|
|
1463
|
+
const world = collected.workflowWorld;
|
|
1464
|
+
const durableWorld = world !== undefined &&
|
|
1465
|
+
world !== "local" &&
|
|
1466
|
+
world !== "@workflow/world-local";
|
|
1467
|
+
if (durableWorld) {
|
|
1468
|
+
resultWarnings.push({
|
|
1469
|
+
id: "EVE_UPDATE_IN_FLIGHT",
|
|
1470
|
+
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.",
|
|
1471
|
+
remediation: "Let pending approvals and long runs settle before updating, or keep step names, queue names, and serialized payloads compatible across the change.",
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
if (prior?.worldCloudflare !==
|
|
1475
|
+
(world === "@moinulmoin/eden-world-cloudflare")) {
|
|
1476
|
+
resultWarnings.push({
|
|
1477
|
+
id: "EVE_UPDATE_WORLD_SWITCH",
|
|
1478
|
+
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.",
|
|
1479
|
+
remediation: "Existing Durable Object data is preserved (classes and bindings are never removed), but runs started before this update remain where they were created.",
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
eveProgress(options, "update: Durable Object data is preserved; the running Container restarts on its next request to serve the new image");
|
|
1483
|
+
}
|
|
1276
1484
|
const accountId = cloudflare.accountId ??
|
|
1277
1485
|
cloudflare.target.identity?.accountId;
|
|
1278
1486
|
const workersDevSubdomain = cloudflare.workersDevSubdomain ??
|
|
@@ -1421,6 +1629,13 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1421
1629
|
...(collected.workflowWorld === undefined
|
|
1422
1630
|
? {}
|
|
1423
1631
|
: { workflowWorld: collected.workflowWorld }),
|
|
1632
|
+
// In-place updates carry the prior Durable Object bindings and
|
|
1633
|
+
// migration history forward verbatim: classes are append-only, never
|
|
1634
|
+
// re-declared, and never deleted (a deleted_classes migration destroys
|
|
1635
|
+
// the data).
|
|
1636
|
+
...(isUpdate && priorDurableObjects !== undefined
|
|
1637
|
+
? { durableObjects: priorDurableObjects }
|
|
1638
|
+
: {}),
|
|
1424
1639
|
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1425
1640
|
? {}
|
|
1426
1641
|
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
@@ -1448,7 +1663,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1448
1663
|
}
|
|
1449
1664
|
catch {
|
|
1450
1665
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-threw-before-terminal-result" });
|
|
1451
|
-
const deployment = deploymentMetadata(identity, targetKey, "indeterminate");
|
|
1666
|
+
const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
|
|
1452
1667
|
return {
|
|
1453
1668
|
...collected.result,
|
|
1454
1669
|
ok: false,
|
|
@@ -1460,7 +1675,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1460
1675
|
return {
|
|
1461
1676
|
...collected.result,
|
|
1462
1677
|
ok: false,
|
|
1463
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1678
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1464
1679
|
};
|
|
1465
1680
|
}
|
|
1466
1681
|
if (publication.status === "failed" ||
|
|
@@ -1472,7 +1687,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1472
1687
|
return {
|
|
1473
1688
|
...collected.result,
|
|
1474
1689
|
ok: false,
|
|
1475
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1690
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1476
1691
|
};
|
|
1477
1692
|
}
|
|
1478
1693
|
if (publication.status === "failed" &&
|
|
@@ -1483,7 +1698,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1483
1698
|
return {
|
|
1484
1699
|
...collected.result,
|
|
1485
1700
|
ok: false,
|
|
1486
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1701
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1487
1702
|
};
|
|
1488
1703
|
}
|
|
1489
1704
|
if (publication.status === "failed" &&
|
|
@@ -1501,7 +1716,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1501
1716
|
return {
|
|
1502
1717
|
...collected.result,
|
|
1503
1718
|
ok: false,
|
|
1504
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1719
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1505
1720
|
};
|
|
1506
1721
|
}
|
|
1507
1722
|
}
|
|
@@ -1513,7 +1728,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1513
1728
|
return {
|
|
1514
1729
|
...collected.result,
|
|
1515
1730
|
ok: false,
|
|
1516
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1731
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1517
1732
|
};
|
|
1518
1733
|
}
|
|
1519
1734
|
let health;
|
|
@@ -1529,7 +1744,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1529
1744
|
return {
|
|
1530
1745
|
...collected.result,
|
|
1531
1746
|
ok: false,
|
|
1532
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1747
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1533
1748
|
};
|
|
1534
1749
|
}
|
|
1535
1750
|
if (health.status === "indeterminate") {
|
|
@@ -1537,7 +1752,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1537
1752
|
return {
|
|
1538
1753
|
...collected.result,
|
|
1539
1754
|
ok: false,
|
|
1540
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1755
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1541
1756
|
};
|
|
1542
1757
|
}
|
|
1543
1758
|
const observedHealthIdentity = health.status === "ready"
|
|
@@ -1554,7 +1769,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1554
1769
|
return {
|
|
1555
1770
|
...collected.result,
|
|
1556
1771
|
ok: false,
|
|
1557
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1772
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1558
1773
|
};
|
|
1559
1774
|
}
|
|
1560
1775
|
if (publication.createdByAttempt === true &&
|
|
@@ -1564,7 +1779,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1564
1779
|
return {
|
|
1565
1780
|
...collected.result,
|
|
1566
1781
|
ok: false,
|
|
1567
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1782
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1568
1783
|
};
|
|
1569
1784
|
}
|
|
1570
1785
|
if (publication.createdByAttempt === true &&
|
|
@@ -1578,7 +1793,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1578
1793
|
return {
|
|
1579
1794
|
...collected.result,
|
|
1580
1795
|
ok: false,
|
|
1581
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1796
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1582
1797
|
};
|
|
1583
1798
|
}
|
|
1584
1799
|
}
|
|
@@ -1590,7 +1805,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1590
1805
|
return {
|
|
1591
1806
|
...collected.result,
|
|
1592
1807
|
ok: false,
|
|
1593
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1808
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1594
1809
|
};
|
|
1595
1810
|
}
|
|
1596
1811
|
try {
|
|
@@ -1609,7 +1824,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1609
1824
|
return {
|
|
1610
1825
|
...collected.result,
|
|
1611
1826
|
ok: false,
|
|
1612
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1827
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1613
1828
|
};
|
|
1614
1829
|
}
|
|
1615
1830
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "failed", {
|
|
@@ -1620,7 +1835,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1620
1835
|
return {
|
|
1621
1836
|
...collected.result,
|
|
1622
1837
|
ok: false,
|
|
1623
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1838
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1624
1839
|
};
|
|
1625
1840
|
}
|
|
1626
1841
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", {
|
|
@@ -1631,7 +1846,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1631
1846
|
return {
|
|
1632
1847
|
...collected.result,
|
|
1633
1848
|
ok: false,
|
|
1634
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1849
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1635
1850
|
};
|
|
1636
1851
|
}
|
|
1637
1852
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "deployed", {
|
|
@@ -1641,8 +1856,33 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1641
1856
|
identity: "exact",
|
|
1642
1857
|
},
|
|
1643
1858
|
});
|
|
1644
|
-
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"),
|
|
1859
|
+
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
|
|
1645
1860
|
await options.afterPromotion?.();
|
|
1861
|
+
let supersededImageCleanup;
|
|
1862
|
+
if (isUpdate) {
|
|
1863
|
+
// The superseded generation's registry repository is generation-scoped
|
|
1864
|
+
// and no longer referenced once CURRENT promotes this generation; only
|
|
1865
|
+
// then is it safe to delete.
|
|
1866
|
+
const previousImage = priorIdentity === undefined
|
|
1867
|
+
? undefined
|
|
1868
|
+
: ownedRegistryImageRef(priorIdentity, request.environment, request.name, accountId);
|
|
1869
|
+
if (previousImage !== undefined) {
|
|
1870
|
+
eveProgress(options, `deleting superseded registry image ${previousImage}`);
|
|
1871
|
+
let deleted = "indeterminate";
|
|
1872
|
+
try {
|
|
1873
|
+
deleted = await (options.deleteRegistryImage ??
|
|
1874
|
+
defaultRegistryImageDelete)({ image: previousImage });
|
|
1875
|
+
}
|
|
1876
|
+
catch {
|
|
1877
|
+
deleted = "indeterminate";
|
|
1878
|
+
}
|
|
1879
|
+
supersededImageCleanup = deleted === "indeterminate"
|
|
1880
|
+
? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry image ${previousImage} could not be proven deleted.`, `Remove only the recorded image ref ${previousImage}; do not retry or run broad registry cleanup.`)
|
|
1881
|
+
: check("VAL-LIFE-007", "passed", deleted === "deleted"
|
|
1882
|
+
? "The superseded generation's registry image was deleted after promotion."
|
|
1883
|
+
: "The superseded generation's registry image was already absent.");
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1646
1886
|
let runtimeImageCleanup;
|
|
1647
1887
|
if (runtimeEvidence.cleanup.imageRetained) {
|
|
1648
1888
|
if (options.retainRuntimeImage === true) {
|
|
@@ -1666,13 +1906,18 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1666
1906
|
: 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
1907
|
}
|
|
1668
1908
|
}
|
|
1909
|
+
const postPromotionChecks = [
|
|
1910
|
+
...(supersededImageCleanup === undefined ? [] : [supersededImageCleanup]),
|
|
1911
|
+
...(runtimeImageCleanup === undefined ? [] : [runtimeImageCleanup]),
|
|
1912
|
+
];
|
|
1669
1913
|
return {
|
|
1670
1914
|
...collected.result,
|
|
1671
|
-
ok: runtimeImageCleanup?.status !== "failed"
|
|
1672
|
-
|
|
1915
|
+
ok: runtimeImageCleanup?.status !== "failed" &&
|
|
1916
|
+
supersededImageCleanup?.status !== "failed",
|
|
1917
|
+
...(postPromotionChecks.length === 0
|
|
1673
1918
|
? {}
|
|
1674
|
-
: { checks: [...collected.result.checks,
|
|
1675
|
-
deployment: deploymentMetadata(identity, targetKey, "deployed"),
|
|
1919
|
+
: { checks: [...collected.result.checks, ...postPromotionChecks] }),
|
|
1920
|
+
deployment: deploymentMetadata(identity, targetKey, "deployed", operation, superseded),
|
|
1676
1921
|
};
|
|
1677
1922
|
}
|
|
1678
1923
|
async function collectEvePreflight(request, options) {
|
|
@@ -1848,12 +2093,14 @@ async function collectEvePreflight(request, options) {
|
|
|
1848
2093
|
}
|
|
1849
2094
|
let cloudflare;
|
|
1850
2095
|
let publicOrigin;
|
|
2096
|
+
let priorDeployment;
|
|
1851
2097
|
if (request.command === "deploy") {
|
|
1852
2098
|
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1853
2099
|
try {
|
|
1854
2100
|
cloudflare = await readEveCloudflareTarget({
|
|
1855
2101
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
1856
2102
|
sourceDigest: candidate.sourceDigest,
|
|
2103
|
+
projectRoot: request.projectRoot,
|
|
1857
2104
|
environment: request.environment,
|
|
1858
2105
|
name: request.name,
|
|
1859
2106
|
}, options);
|
|
@@ -1875,6 +2122,17 @@ async function collectEvePreflight(request, options) {
|
|
|
1875
2122
|
};
|
|
1876
2123
|
}
|
|
1877
2124
|
checks.push(...cloudflareChecks(cloudflare));
|
|
2125
|
+
// An owned target also requires its immutable record so the update can
|
|
2126
|
+
// carry the published Durable Object history forward append-only.
|
|
2127
|
+
if (cloudflare.target.state === "owned") {
|
|
2128
|
+
const record = await readEveDeploymentRecord(request.projectRoot, request.environment, request.name, cloudflare.accountId, packaging.project?.projectId === "unknown-project"
|
|
2129
|
+
? undefined
|
|
2130
|
+
: packaging.project?.projectId);
|
|
2131
|
+
priorDeployment = record ?? undefined;
|
|
2132
|
+
if (record === undefined || record.durableObjects === undefined) {
|
|
2133
|
+
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."));
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
1878
2136
|
publicOrigin = await resolvedCloudflareOrigin(cloudflare, request.name);
|
|
1879
2137
|
if (publicOrigin === undefined) {
|
|
1880
2138
|
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 +2264,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2006
2264
|
cloudflare = await readEveCloudflareTarget({
|
|
2007
2265
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
2008
2266
|
sourceDigest: candidate.sourceDigest,
|
|
2267
|
+
projectRoot: request.projectRoot,
|
|
2009
2268
|
environment: request.environment,
|
|
2010
2269
|
name: request.name,
|
|
2011
2270
|
}, options);
|
|
@@ -2049,6 +2308,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2049
2308
|
// reported as EVE_SCHEDULE_UNSUPPORTED and fire only while awake.
|
|
2050
2309
|
schedules: expressible,
|
|
2051
2310
|
cloudflare,
|
|
2311
|
+
...(priorDeployment === undefined ? {} : { priorDeployment }),
|
|
2052
2312
|
...(workflowWorld === undefined ? {} : { workflowWorld }),
|
|
2053
2313
|
};
|
|
2054
2314
|
}
|
|
@@ -2095,7 +2355,7 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
2095
2355
|
message: status === "indeterminate"
|
|
2096
2356
|
? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
|
|
2097
2357
|
: status === "deployed"
|
|
2098
|
-
? "The Eve deployment is healthy and promoted, but exact
|
|
2358
|
+
? "The Eve deployment is healthy and promoted, but exact image cleanup failed; do not retry the deployment."
|
|
2099
2359
|
: "The Eve deployment failed before exact target promotion.",
|
|
2100
2360
|
});
|
|
2101
2361
|
}
|
|
@@ -2223,6 +2483,172 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
2223
2483
|
identity: verified,
|
|
2224
2484
|
};
|
|
2225
2485
|
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Reads the immutable records for this target. The live identity comes from
|
|
2488
|
+
* CURRENT first, then the newest `deployed` record, then the newest record
|
|
2489
|
+
* of any status; the Durable Object history comes from the newest record
|
|
2490
|
+
* that carries one, healthy or not, so a failed update's published tags are
|
|
2491
|
+
* never dropped.
|
|
2492
|
+
*/
|
|
2493
|
+
async function readEveDeploymentRecord(projectRoot, environment, name, accountId, projectId) {
|
|
2494
|
+
const targetsRoot = join(projectRoot, ".eden", "eve-deploy", "targets", `${environment}-${name}`);
|
|
2495
|
+
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2496
|
+
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2497
|
+
const scanned = [];
|
|
2498
|
+
const deploymentIds = new Set();
|
|
2499
|
+
const images = new Set();
|
|
2500
|
+
for (const entry of entries) {
|
|
2501
|
+
const generationRoot = join(generationsRoot, entry);
|
|
2502
|
+
const recordPath = join(generationRoot, "deployment.json");
|
|
2503
|
+
const details = await lstat(recordPath).catch(() => undefined);
|
|
2504
|
+
if (details === undefined || !details.isFile())
|
|
2505
|
+
continue;
|
|
2506
|
+
const parsed = await parseEveDeploymentRecord(recordPath, environment, name);
|
|
2507
|
+
if (parsed !== undefined) {
|
|
2508
|
+
// Only records for this account and (when known) this project may
|
|
2509
|
+
// arbitrate ownership or supply migration history.
|
|
2510
|
+
if ((accountId !== undefined && parsed.identity.accountId !== accountId) ||
|
|
2511
|
+
(projectId !== undefined && parsed.identity.projectId !== projectId)) {
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
scanned.push({
|
|
2515
|
+
record: parsed,
|
|
2516
|
+
generationRoot,
|
|
2517
|
+
modifiedAt: details.mtimeMs,
|
|
2518
|
+
});
|
|
2519
|
+
const deploymentId = parsed.deploymentId;
|
|
2520
|
+
if (deploymentId !== undefined)
|
|
2521
|
+
deploymentIds.add(deploymentId);
|
|
2522
|
+
if (typeof parsed.identity.containerImage === "string") {
|
|
2523
|
+
images.add(parsed.identity.containerImage);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
scanned.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
2528
|
+
const current = await lstat(join(targetsRoot, "CURRENT")).catch(() => undefined);
|
|
2529
|
+
let live = scanned.find((entry) => entry.record.status === "deployed") ?? scanned[0];
|
|
2530
|
+
if (current !== undefined && current.isSymbolicLink()) {
|
|
2531
|
+
const generationRoot = await realpath(join(targetsRoot, "CURRENT")).catch(() => undefined);
|
|
2532
|
+
if (generationRoot !== undefined) {
|
|
2533
|
+
const pinned = scanned.find((entry) => entry.generationRoot === generationRoot);
|
|
2534
|
+
if (pinned !== undefined) {
|
|
2535
|
+
live = pinned;
|
|
2536
|
+
}
|
|
2537
|
+
else {
|
|
2538
|
+
const parsed = await parseEveDeploymentRecord(join(generationRoot, "deployment.json"), environment, name);
|
|
2539
|
+
if (parsed !== undefined &&
|
|
2540
|
+
!((accountId !== undefined &&
|
|
2541
|
+
parsed.identity.accountId !== accountId) ||
|
|
2542
|
+
(projectId !== undefined &&
|
|
2543
|
+
parsed.identity.projectId !== projectId))) {
|
|
2544
|
+
live = { record: parsed, generationRoot, modifiedAt: 0 };
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (live === undefined)
|
|
2550
|
+
return undefined;
|
|
2551
|
+
// Newest record carrying a usable DO history wins, regardless of its
|
|
2552
|
+
// deployment outcome: a tag it published must never be replayed.
|
|
2553
|
+
const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
|
|
2554
|
+
return {
|
|
2555
|
+
recordPath: live.record.recordPath,
|
|
2556
|
+
generationRoot: live.generationRoot,
|
|
2557
|
+
identity: live.record.identity,
|
|
2558
|
+
status: live.record.status,
|
|
2559
|
+
deploymentIds: [...deploymentIds],
|
|
2560
|
+
images: [...images],
|
|
2561
|
+
worldCloudflare: live.record.worldCloudflare,
|
|
2562
|
+
durableObjects: historySource?.record.durableObjects,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
2566
|
+
const base = await parseEveDestroyRecord(recordPath, environment, name);
|
|
2567
|
+
if (base === undefined)
|
|
2568
|
+
return undefined;
|
|
2569
|
+
let parsed;
|
|
2570
|
+
try {
|
|
2571
|
+
parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
2572
|
+
}
|
|
2573
|
+
catch {
|
|
2574
|
+
return undefined;
|
|
2575
|
+
}
|
|
2576
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2577
|
+
return undefined;
|
|
2578
|
+
const record = parsed;
|
|
2579
|
+
const identityRaw = parsed.identity;
|
|
2580
|
+
const deploymentId = typeof identityRaw === "object" &&
|
|
2581
|
+
identityRaw !== null &&
|
|
2582
|
+
typeof identityRaw
|
|
2583
|
+
.deploymentId === "string"
|
|
2584
|
+
? identityRaw.deploymentId
|
|
2585
|
+
: undefined;
|
|
2586
|
+
const status = typeof record.status === "string" &&
|
|
2587
|
+
["deployed", "failed", "indeterminate"].includes(record.status)
|
|
2588
|
+
? record.status
|
|
2589
|
+
: undefined;
|
|
2590
|
+
const worker = typeof record.worker === "object" && record.worker !== null
|
|
2591
|
+
? record.worker
|
|
2592
|
+
: undefined;
|
|
2593
|
+
const rawBindings = worker !== undefined && "durable_objects" in worker &&
|
|
2594
|
+
typeof worker.durable_objects === "object" &&
|
|
2595
|
+
worker.durable_objects !== null &&
|
|
2596
|
+
"bindings" in worker.durable_objects
|
|
2597
|
+
? worker.durable_objects.bindings
|
|
2598
|
+
: undefined;
|
|
2599
|
+
const rawMigrations = worker !== undefined && "migrations" in worker
|
|
2600
|
+
? worker.migrations
|
|
2601
|
+
: undefined;
|
|
2602
|
+
const rawVars = worker !== undefined && "vars" in worker &&
|
|
2603
|
+
typeof worker.vars === "object" && worker.vars !== null
|
|
2604
|
+
? worker.vars
|
|
2605
|
+
: undefined;
|
|
2606
|
+
const bindings = [];
|
|
2607
|
+
const migrations = [];
|
|
2608
|
+
const durableObjectsValid = Array.isArray(rawBindings) &&
|
|
2609
|
+
Array.isArray(rawMigrations);
|
|
2610
|
+
if (durableObjectsValid) {
|
|
2611
|
+
for (const binding of rawBindings) {
|
|
2612
|
+
if (typeof binding !== "object" || binding === null ||
|
|
2613
|
+
typeof binding.name !== "string" ||
|
|
2614
|
+
typeof binding.class_name !== "string") {
|
|
2615
|
+
return {
|
|
2616
|
+
recordPath,
|
|
2617
|
+
identity: base.identity,
|
|
2618
|
+
status,
|
|
2619
|
+
deploymentId,
|
|
2620
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2621
|
+
durableObjects: undefined,
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
bindings.push(binding);
|
|
2625
|
+
}
|
|
2626
|
+
for (const migration of rawMigrations) {
|
|
2627
|
+
if (typeof migration !== "object" || migration === null ||
|
|
2628
|
+
typeof migration.tag !== "string" ||
|
|
2629
|
+
!Array.isArray(migration.new_sqlite_classes) ||
|
|
2630
|
+
migration.new_sqlite_classes.some((className) => typeof className !== "string")) {
|
|
2631
|
+
return {
|
|
2632
|
+
recordPath,
|
|
2633
|
+
identity: base.identity,
|
|
2634
|
+
status,
|
|
2635
|
+
deploymentId,
|
|
2636
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2637
|
+
durableObjects: undefined,
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
migrations.push(migration);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return {
|
|
2644
|
+
recordPath,
|
|
2645
|
+
identity: base.identity,
|
|
2646
|
+
status,
|
|
2647
|
+
deploymentId,
|
|
2648
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2649
|
+
durableObjects: durableObjectsValid ? { bindings, migrations } : undefined,
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2226
2652
|
async function defaultDestroyTargetRead(request) {
|
|
2227
2653
|
const account = await readWranglerJson(["whoami", "--json"]);
|
|
2228
2654
|
const deployments = await readWranglerJson([
|
package/dist/eve.js
CHANGED
|
@@ -49,7 +49,7 @@ Options:
|
|
|
49
49
|
`,
|
|
50
50
|
deploy: `Usage: eden deploy [--project <path>] [--env <preview|production>] [--name <name>] [--env-file <path>] [--json]
|
|
51
51
|
|
|
52
|
-
Deploy the selected Eve project to one exact target after host checks pass.
|
|
52
|
+
Deploy the selected Eve project to one exact target after host checks pass. When the exact target already exists and Eden's immutable record proves it owns it, deploy updates the Worker in place and keeps Durable Object data; a target Eden cannot prove it owns still fails the conflict check.
|
|
53
53
|
|
|
54
54
|
Options:
|
|
55
55
|
--project <path> Eve project root (default: current directory)
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { EveCliCommand, EveCliEnvironment, EveCliExecutionRequest, EveCliHe
|
|
|
10
10
|
export type { EveNodeImage, EvePackagingCheck, EvePackagingCode, EveProjectBuilder, EveProjectBuilderRequest, EveProjectBuilderResult, EveProjectBuildCandidate, EveProjectFile, EveProjectImage, EveProjectInputManifest, EveProjectOutput, EveProjectPackagingResult, EveProjectRuntime, EveProjectSnapshot, EveProjectSnapshotOptions, EveProjectToolchain, EveRuntimeConfigExclusion, } from "./eve-packaging.js";
|
|
11
11
|
export type { EveHostRequirements, EveRuntimeClosure, EveRuntimeClosureFile, EveRuntimeCleanup, EveRuntimeImage, EveRuntimeImageDiscardRequest, EveRuntimeImageMetadata, EveRuntimeImageRequest, EveRuntimeImageResult, EveRuntimeNativeModule, EveRuntimeStatus, } from "./eve-runtime-image.js";
|
|
12
12
|
export { DEFAULT_EVE_HOST_REQUIREMENTS, DEFAULT_EVE_NODE_IMAGE, runEveControlPlane, runEveDestroy, } from "./eve-control-plane.js";
|
|
13
|
-
export type { EveCloudflareReadRequest, EveCloudflareReadResult, EveCloudflareReadRunner, EveCloudflareTargetState, EveContainerDeleteRunner, EveDeploymentCompensationRequest, EveDeploymentCompensationRunner, EveDeploymentHealthRequest, EveDeploymentHealthResult, EveDeploymentHealthRunner, EveDeploymentIdentity, EveDeploymentIdentityProof, EveDeploymentMetadata, EveDeploymentPublicationRequest, EveDeploymentPublicationResult, EveDeploymentPublicationRunner, EveDeploymentStatus, EveDestroyCloudflareReadRequest, EveDestroyCloudflareReadRunner, EveDestroyOutcome, EveDestroyTargetRead, EveImagePublicationRequest, EveImagePublicationResult, EveImagePublicationRunner, EvePreflightCandidate, EvePreflightResult, EvePreflightRuntimeEvidence, EvePreflightWarning, EvePreflightRuntimeRunner, EvePreflightRuntimeRunnerRequest, EveRuntimeConfigLoader, EveRuntimeImageDiscardRunner, EveWorkerDeleteRunner, } from "./eve-control-plane.js";
|
|
13
|
+
export type { EveCloudflareReadRequest, EveCloudflareReadResult, EveCloudflareReadRunner, EveCloudflareTargetState, EveContainerDeleteRunner, EveDeploymentCompensationRequest, EveDeploymentCompensationRunner, EveDeploymentHealthRequest, EveDeploymentHealthResult, EveDeploymentHealthRunner, EveDeploymentIdentity, EveDeploymentIdentityProof, EveDeploymentMetadata, EveDeploymentPublicationRequest, EveDeploymentPublicationResult, EveDeploymentPublicationRunner, EveDeploymentStatus, EveDestroyCloudflareReadRequest, EveDestroyCloudflareReadRunner, EveDestroyOutcome, EveDestroyTargetRead, EveImagePublicationRequest, EveImagePublicationResult, EveImagePublicationRunner, EvePreflightCandidate, EvePreflightResult, EvePreflightRuntimeEvidence, EvePreflightWarning, EvePreflightRuntimeRunner, EvePreflightRuntimeRunnerRequest, EveRegistryImageDeleteRunner, EveRuntimeConfigLoader, EveRuntimeImageDiscardRunner, EveWorkerDeleteRunner, } from "./eve-control-plane.js";
|
|
14
14
|
export declare const EDEN_CLI_COMMANDS: readonly ["preflight", "deploy", "destroy"];
|
|
15
15
|
export type EdenCliCommand = (typeof EDEN_CLI_COMMANDS)[number];
|
|
16
16
|
export interface EdenCliRunOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moinulmoin/eden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Deploy existing Eve projects to your own Cloudflare account.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"NOTICE"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@moinulmoin/eden-runtime-cloudflare": "0.
|
|
34
|
+
"@moinulmoin/eden-runtime-cloudflare": "0.5.0",
|
|
35
35
|
"wrangler": "4.120.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|