@moinulmoin/eden 0.3.0 → 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/NOTICE +8 -0
- package/README.md +3 -3
- package/dist/eve-control-plane.d.ts +38 -1
- package/dist/eve-control-plane.js +479 -45
- package/dist/eve-runtime-config.d.ts +1 -1
- package/dist/eve-runtime-config.js +3 -1
- package/dist/eve-runtime-image.js +6 -0
- package/dist/eve.js +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -2
package/NOTICE
CHANGED
|
@@ -9,3 +9,11 @@ Eden's generated Worker host bundle includes software from
|
|
|
9
9
|
`@cloudflare/containers`, distributed under MIT OR Apache-2.0. Eden
|
|
10
10
|
redistributes that bundled software under the Apache-2.0 option included in
|
|
11
11
|
LICENSE.
|
|
12
|
+
|
|
13
|
+
Workflow SDK
|
|
14
|
+
@workflow/world-postgres, version 5.0.0-beta.47
|
|
15
|
+
https://github.com/vercel/workflow
|
|
16
|
+
|
|
17
|
+
Eden's Cloudflare World storage implementation
|
|
18
|
+
(`@moinulmoin/eden-world-cloudflare`) is adapted from the Workflow SDK
|
|
19
|
+
PostgreSQL World, distributed under the Apache License, Version 2.0.
|
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 {
|
|
@@ -142,12 +142,13 @@ async function readInstalledManifest(snapshotRoot, packageName) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
* Fails deploy/preflight when
|
|
146
|
-
*
|
|
145
|
+
* Fails deploy/preflight when a known durable World would trip Eve's runtime
|
|
146
|
+
* World-compatibility rejection. Unreadable manifests
|
|
147
147
|
* mirror Eve's own behavior (skip); Eve is the runtime authority.
|
|
148
148
|
*/
|
|
149
149
|
async function checkEveWorldPairing(candidate, world) {
|
|
150
|
-
if (world !== "@workflow/world-postgres"
|
|
150
|
+
if (world !== "@workflow/world-postgres" &&
|
|
151
|
+
world !== "@moinulmoin/eden-world-cloudflare")
|
|
151
152
|
return undefined;
|
|
152
153
|
const eveManifest = await readInstalledManifest(candidate.snapshotRoot, "eve");
|
|
153
154
|
const expectedRange = eveManifest === undefined
|
|
@@ -183,9 +184,9 @@ async function checkEveWorldPairing(candidate, world) {
|
|
|
183
184
|
expectedLine.prereleaseTag !== undefined &&
|
|
184
185
|
declaredLine.prereleaseTag !== expectedLine.prereleaseTag);
|
|
185
186
|
if (mismatch) {
|
|
186
|
-
return check("EVE_WORLD_PAIRING", "failed", `The installed
|
|
187
|
+
return check("EVE_WORLD_PAIRING", "failed", `The installed ${world} targets ${declared.packageName} ${declared.range}, but the project's Eve bundles @workflow/core ${expectedRange}; Eve rejects this World at runtime.`, `Install the ${world} release whose ${declared.packageName} dependency matches the Eve-bundled @workflow/core ${expectedRange} line; see docs/deploy.md.`);
|
|
187
188
|
}
|
|
188
|
-
return check("EVE_WORLD_PAIRING", "passed", `The installed
|
|
189
|
+
return check("EVE_WORLD_PAIRING", "passed", `The installed ${world} ${declared.packageName} dependency ${declared.range} pairs with the Eve-bundled @workflow/core ${expectedRange}.`);
|
|
189
190
|
}
|
|
190
191
|
/**
|
|
191
192
|
* Returns a generic reason when the URL targets a transaction-mode pooler.
|
|
@@ -289,7 +290,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
289
290
|
if (result.ok) {
|
|
290
291
|
if (result.command === "eve deploy") {
|
|
291
292
|
const url = result.deployment?.stableWorkersDevOrigin;
|
|
292
|
-
lines.push("", `✓ deployed ${result.name} (${result.environment})` +
|
|
293
|
+
lines.push("", `✓ ${result.deployment?.operation === "update" ? "updated" : "deployed"} ${result.name} (${result.environment})` +
|
|
293
294
|
(eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
|
|
294
295
|
if (url !== undefined)
|
|
295
296
|
lines.push(url);
|
|
@@ -305,7 +306,7 @@ function humanEveResultLines(result, startedAt, eveVersion) {
|
|
|
305
306
|
lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
|
|
306
307
|
? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
|
|
307
308
|
: result.deployment.status === "deployed"
|
|
308
|
-
? "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."
|
|
309
310
|
: "The deployment failed before promotion; the exact target was not claimed."}`);
|
|
310
311
|
}
|
|
311
312
|
else if (firstFailure !== undefined) {
|
|
@@ -441,8 +442,21 @@ function exactContainerEntries(value, name) {
|
|
|
441
442
|
}
|
|
442
443
|
const id = entry.id;
|
|
443
444
|
const entryName = entry.name;
|
|
444
|
-
if (entryName === name)
|
|
445
|
-
|
|
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
|
+
}
|
|
446
460
|
}
|
|
447
461
|
if (entries.length === 0 && collection.length >= 100) {
|
|
448
462
|
return undefined;
|
|
@@ -594,6 +608,9 @@ async function defaultCloudflareRead(request) {
|
|
|
594
608
|
target: { state: "absent" },
|
|
595
609
|
};
|
|
596
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);
|
|
597
614
|
return {
|
|
598
615
|
accountAccess,
|
|
599
616
|
containerAccess,
|
|
@@ -604,16 +621,141 @@ async function defaultCloudflareRead(request) {
|
|
|
604
621
|
? {}
|
|
605
622
|
: { workersDevSubdomain }),
|
|
606
623
|
target: {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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.",
|
|
610
751
|
},
|
|
611
752
|
};
|
|
612
753
|
}
|
|
613
754
|
async function readEveCloudflareTarget(request, options) {
|
|
614
|
-
|
|
615
|
-
? defaultCloudflareRead(request)
|
|
616
|
-
: options.cloudflareRead(request);
|
|
755
|
+
const result = options.cloudflareRead === undefined
|
|
756
|
+
? await defaultCloudflareRead(request)
|
|
757
|
+
: await options.cloudflareRead(request);
|
|
758
|
+
return resolveEveTargetOwnership(request, result);
|
|
617
759
|
}
|
|
618
760
|
async function resolvedCloudflareOrigin(result, workerName) {
|
|
619
761
|
if (result.accountId === undefined ||
|
|
@@ -712,12 +854,12 @@ function cloudflareChecks(result) {
|
|
|
712
854
|
? undefined
|
|
713
855
|
: "Use an authenticated read-only adapter with account and Container access; preflight never uploads, publishes, or changes remote state.";
|
|
714
856
|
const target = result.target;
|
|
715
|
-
const targetPasses = target.state === "absent" ||
|
|
716
|
-
(target.state === "owned" && target.matchesCandidate === true);
|
|
857
|
+
const targetPasses = target.state === "absent" || target.state === "owned";
|
|
717
858
|
const targetMessage = targetPasses
|
|
718
859
|
? target.state === "absent"
|
|
719
860
|
? "The exact target read is absent and is eligible without claiming ownership."
|
|
720
|
-
:
|
|
861
|
+
: target.message ??
|
|
862
|
+
"The exact target is provably Eden-owned; deploy proceeds as an in-place update."
|
|
721
863
|
: target.message ??
|
|
722
864
|
"The exact target read is unowned, mismatched, cross-environment, or ambiguous.";
|
|
723
865
|
const targetRemediation = targetPasses
|
|
@@ -1061,6 +1203,10 @@ function responseIdentity(response, body) {
|
|
|
1061
1203
|
read("workerName", "x-eden-eve-worker-name");
|
|
1062
1204
|
read("containerApplicationName", "x-eden-eve-container-application");
|
|
1063
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
|
+
}
|
|
1064
1210
|
read("containerImage", "x-eden-eve-container-image");
|
|
1065
1211
|
return identity;
|
|
1066
1212
|
}
|
|
@@ -1104,9 +1250,20 @@ function defaultHealthRunner() {
|
|
|
1104
1250
|
body.state === "ready" ||
|
|
1105
1251
|
body.ready === true);
|
|
1106
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
|
+
}
|
|
1107
1264
|
return {
|
|
1108
1265
|
status: "ready",
|
|
1109
|
-
identity:
|
|
1266
|
+
identity: observedIdentity,
|
|
1110
1267
|
};
|
|
1111
1268
|
}
|
|
1112
1269
|
lastReason =
|
|
@@ -1160,8 +1317,13 @@ async function releaseEveTargetLock(lock) {
|
|
|
1160
1317
|
await rm(lock.path, { force: true });
|
|
1161
1318
|
return (await lstat(lock.path).catch(() => undefined)) === undefined;
|
|
1162
1319
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
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);
|
|
1165
1327
|
const artifactBoundary = await realpath(dirname(deploymentRoot)).catch(() => undefined);
|
|
1166
1328
|
const generationCanonical = await realpath(generationRoot).catch(() => undefined);
|
|
1167
1329
|
if (artifactBoundary === undefined ||
|
|
@@ -1190,7 +1352,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1190
1352
|
throw new EveCliError({
|
|
1191
1353
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1192
1354
|
message: "The Eve target pointer directory could not be verified safely.",
|
|
1193
|
-
source:
|
|
1355
|
+
source: pointerKey,
|
|
1194
1356
|
});
|
|
1195
1357
|
}
|
|
1196
1358
|
const currentPath = join(targetRoot, "CURRENT");
|
|
@@ -1207,7 +1369,7 @@ async function promoteEveTargetPointer(deploymentRoot, targetKey, generationRoot
|
|
|
1207
1369
|
throw new EveCliError({
|
|
1208
1370
|
code: "EVE_DEPLOY_PROMOTION_FAILED",
|
|
1209
1371
|
message: "The Eve target pointer did not resolve to the promoted generation.",
|
|
1210
|
-
source:
|
|
1372
|
+
source: pointerKey,
|
|
1211
1373
|
});
|
|
1212
1374
|
}
|
|
1213
1375
|
return currentPath;
|
|
@@ -1241,9 +1403,10 @@ async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
|
|
|
1241
1403
|
flag: "wx",
|
|
1242
1404
|
});
|
|
1243
1405
|
}
|
|
1244
|
-
function deploymentMetadata(identity, targetKey, status) {
|
|
1406
|
+
function deploymentMetadata(identity, targetKey, status, operation, superseded) {
|
|
1245
1407
|
return {
|
|
1246
1408
|
status,
|
|
1409
|
+
operation,
|
|
1247
1410
|
targetKey,
|
|
1248
1411
|
deploymentId: identity.deploymentId,
|
|
1249
1412
|
workerName: identity.workerName,
|
|
@@ -1251,6 +1414,12 @@ function deploymentMetadata(identity, targetKey, status) {
|
|
|
1251
1414
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1252
1415
|
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1253
1416
|
imageReference: identity.containerImage,
|
|
1417
|
+
...(superseded?.generationId === undefined
|
|
1418
|
+
? {}
|
|
1419
|
+
: { supersededGenerationId: superseded.generationId }),
|
|
1420
|
+
...(superseded?.containerImage === undefined
|
|
1421
|
+
? {}
|
|
1422
|
+
: { supersededImageReference: superseded.containerImage }),
|
|
1254
1423
|
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1255
1424
|
evidenceRetained: true,
|
|
1256
1425
|
};
|
|
@@ -1272,6 +1441,46 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1272
1441
|
collected.projectId === undefined) {
|
|
1273
1442
|
throw deploymentFailure("EVE_DEPLOY_CHECKS_FAILED", "The inline Eve deploy checks did not return a complete immutable candidate.");
|
|
1274
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
|
+
}
|
|
1275
1484
|
const accountId = cloudflare.accountId ??
|
|
1276
1485
|
cloudflare.target.identity?.accountId;
|
|
1277
1486
|
const workersDevSubdomain = cloudflare.workersDevSubdomain ??
|
|
@@ -1417,6 +1626,16 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1417
1626
|
? {}
|
|
1418
1627
|
: { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
|
|
1419
1628
|
schedules: collected.schedules ?? [],
|
|
1629
|
+
...(collected.workflowWorld === undefined
|
|
1630
|
+
? {}
|
|
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
|
+
: {}),
|
|
1420
1639
|
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1421
1640
|
? {}
|
|
1422
1641
|
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
@@ -1444,7 +1663,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1444
1663
|
}
|
|
1445
1664
|
catch {
|
|
1446
1665
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-threw-before-terminal-result" });
|
|
1447
|
-
const deployment = deploymentMetadata(identity, targetKey, "indeterminate");
|
|
1666
|
+
const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
|
|
1448
1667
|
return {
|
|
1449
1668
|
...collected.result,
|
|
1450
1669
|
ok: false,
|
|
@@ -1456,7 +1675,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1456
1675
|
return {
|
|
1457
1676
|
...collected.result,
|
|
1458
1677
|
ok: false,
|
|
1459
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1678
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1460
1679
|
};
|
|
1461
1680
|
}
|
|
1462
1681
|
if (publication.status === "failed" ||
|
|
@@ -1468,7 +1687,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1468
1687
|
return {
|
|
1469
1688
|
...collected.result,
|
|
1470
1689
|
ok: false,
|
|
1471
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1690
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1472
1691
|
};
|
|
1473
1692
|
}
|
|
1474
1693
|
if (publication.status === "failed" &&
|
|
@@ -1479,7 +1698,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1479
1698
|
return {
|
|
1480
1699
|
...collected.result,
|
|
1481
1700
|
ok: false,
|
|
1482
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1701
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1483
1702
|
};
|
|
1484
1703
|
}
|
|
1485
1704
|
if (publication.status === "failed" &&
|
|
@@ -1497,7 +1716,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1497
1716
|
return {
|
|
1498
1717
|
...collected.result,
|
|
1499
1718
|
ok: false,
|
|
1500
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1719
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1501
1720
|
};
|
|
1502
1721
|
}
|
|
1503
1722
|
}
|
|
@@ -1509,7 +1728,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1509
1728
|
return {
|
|
1510
1729
|
...collected.result,
|
|
1511
1730
|
ok: false,
|
|
1512
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1731
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1513
1732
|
};
|
|
1514
1733
|
}
|
|
1515
1734
|
let health;
|
|
@@ -1525,7 +1744,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1525
1744
|
return {
|
|
1526
1745
|
...collected.result,
|
|
1527
1746
|
ok: false,
|
|
1528
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1747
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1529
1748
|
};
|
|
1530
1749
|
}
|
|
1531
1750
|
if (health.status === "indeterminate") {
|
|
@@ -1533,7 +1752,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1533
1752
|
return {
|
|
1534
1753
|
...collected.result,
|
|
1535
1754
|
ok: false,
|
|
1536
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1755
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1537
1756
|
};
|
|
1538
1757
|
}
|
|
1539
1758
|
const observedHealthIdentity = health.status === "ready"
|
|
@@ -1550,7 +1769,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1550
1769
|
return {
|
|
1551
1770
|
...collected.result,
|
|
1552
1771
|
ok: false,
|
|
1553
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1772
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1554
1773
|
};
|
|
1555
1774
|
}
|
|
1556
1775
|
if (publication.createdByAttempt === true &&
|
|
@@ -1560,7 +1779,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1560
1779
|
return {
|
|
1561
1780
|
...collected.result,
|
|
1562
1781
|
ok: false,
|
|
1563
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1782
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1564
1783
|
};
|
|
1565
1784
|
}
|
|
1566
1785
|
if (publication.createdByAttempt === true &&
|
|
@@ -1574,7 +1793,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1574
1793
|
return {
|
|
1575
1794
|
...collected.result,
|
|
1576
1795
|
ok: false,
|
|
1577
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1796
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1578
1797
|
};
|
|
1579
1798
|
}
|
|
1580
1799
|
}
|
|
@@ -1586,7 +1805,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1586
1805
|
return {
|
|
1587
1806
|
...collected.result,
|
|
1588
1807
|
ok: false,
|
|
1589
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1808
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1590
1809
|
};
|
|
1591
1810
|
}
|
|
1592
1811
|
try {
|
|
@@ -1605,7 +1824,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1605
1824
|
return {
|
|
1606
1825
|
...collected.result,
|
|
1607
1826
|
ok: false,
|
|
1608
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1827
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1609
1828
|
};
|
|
1610
1829
|
}
|
|
1611
1830
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "failed", {
|
|
@@ -1616,7 +1835,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1616
1835
|
return {
|
|
1617
1836
|
...collected.result,
|
|
1618
1837
|
ok: false,
|
|
1619
|
-
deployment: deploymentMetadata(identity, targetKey, "failed"),
|
|
1838
|
+
deployment: deploymentMetadata(identity, targetKey, "failed", operation, superseded),
|
|
1620
1839
|
};
|
|
1621
1840
|
}
|
|
1622
1841
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", {
|
|
@@ -1627,7 +1846,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1627
1846
|
return {
|
|
1628
1847
|
...collected.result,
|
|
1629
1848
|
ok: false,
|
|
1630
|
-
deployment: deploymentMetadata(identity, targetKey, "indeterminate"),
|
|
1849
|
+
deployment: deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded),
|
|
1631
1850
|
};
|
|
1632
1851
|
}
|
|
1633
1852
|
await writeEveDeploymentRecord(candidate, identity, hostConfig, "deployed", {
|
|
@@ -1637,8 +1856,33 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1637
1856
|
identity: "exact",
|
|
1638
1857
|
},
|
|
1639
1858
|
});
|
|
1640
|
-
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"),
|
|
1859
|
+
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
|
|
1641
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
|
+
}
|
|
1642
1886
|
let runtimeImageCleanup;
|
|
1643
1887
|
if (runtimeEvidence.cleanup.imageRetained) {
|
|
1644
1888
|
if (options.retainRuntimeImage === true) {
|
|
@@ -1662,13 +1906,18 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1662
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.`);
|
|
1663
1907
|
}
|
|
1664
1908
|
}
|
|
1909
|
+
const postPromotionChecks = [
|
|
1910
|
+
...(supersededImageCleanup === undefined ? [] : [supersededImageCleanup]),
|
|
1911
|
+
...(runtimeImageCleanup === undefined ? [] : [runtimeImageCleanup]),
|
|
1912
|
+
];
|
|
1665
1913
|
return {
|
|
1666
1914
|
...collected.result,
|
|
1667
|
-
ok: runtimeImageCleanup?.status !== "failed"
|
|
1668
|
-
|
|
1915
|
+
ok: runtimeImageCleanup?.status !== "failed" &&
|
|
1916
|
+
supersededImageCleanup?.status !== "failed",
|
|
1917
|
+
...(postPromotionChecks.length === 0
|
|
1669
1918
|
? {}
|
|
1670
|
-
: { checks: [...collected.result.checks,
|
|
1671
|
-
deployment: deploymentMetadata(identity, targetKey, "deployed"),
|
|
1919
|
+
: { checks: [...collected.result.checks, ...postPromotionChecks] }),
|
|
1920
|
+
deployment: deploymentMetadata(identity, targetKey, "deployed", operation, superseded),
|
|
1672
1921
|
};
|
|
1673
1922
|
}
|
|
1674
1923
|
async function collectEvePreflight(request, options) {
|
|
@@ -1804,6 +2053,9 @@ async function collectEvePreflight(request, options) {
|
|
|
1804
2053
|
}
|
|
1805
2054
|
const candidate = packaging.candidate;
|
|
1806
2055
|
const workflowWorld = await readCompiledWorldSpecifier(candidate);
|
|
2056
|
+
if (workflowWorld === "@moinulmoin/eden-world-cloudflare") {
|
|
2057
|
+
checks.push(check("EVE-WORLD", "passed", "world: Cloudflare Durable Object"));
|
|
2058
|
+
}
|
|
1807
2059
|
warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
|
|
1808
2060
|
const schedules = await readCompiledSchedules(candidate);
|
|
1809
2061
|
// The runtime package is imported lazily so `eden` commands that never
|
|
@@ -1841,12 +2093,14 @@ async function collectEvePreflight(request, options) {
|
|
|
1841
2093
|
}
|
|
1842
2094
|
let cloudflare;
|
|
1843
2095
|
let publicOrigin;
|
|
2096
|
+
let priorDeployment;
|
|
1844
2097
|
if (request.command === "deploy") {
|
|
1845
2098
|
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1846
2099
|
try {
|
|
1847
2100
|
cloudflare = await readEveCloudflareTarget({
|
|
1848
2101
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
1849
2102
|
sourceDigest: candidate.sourceDigest,
|
|
2103
|
+
projectRoot: request.projectRoot,
|
|
1850
2104
|
environment: request.environment,
|
|
1851
2105
|
name: request.name,
|
|
1852
2106
|
}, options);
|
|
@@ -1868,6 +2122,17 @@ async function collectEvePreflight(request, options) {
|
|
|
1868
2122
|
};
|
|
1869
2123
|
}
|
|
1870
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
|
+
}
|
|
1871
2136
|
publicOrigin = await resolvedCloudflareOrigin(cloudflare, request.name);
|
|
1872
2137
|
if (publicOrigin === undefined) {
|
|
1873
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."));
|
|
@@ -1999,6 +2264,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1999
2264
|
cloudflare = await readEveCloudflareTarget({
|
|
2000
2265
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
2001
2266
|
sourceDigest: candidate.sourceDigest,
|
|
2267
|
+
projectRoot: request.projectRoot,
|
|
2002
2268
|
environment: request.environment,
|
|
2003
2269
|
name: request.name,
|
|
2004
2270
|
}, options);
|
|
@@ -2042,6 +2308,8 @@ async function collectEvePreflight(request, options) {
|
|
|
2042
2308
|
// reported as EVE_SCHEDULE_UNSUPPORTED and fire only while awake.
|
|
2043
2309
|
schedules: expressible,
|
|
2044
2310
|
cloudflare,
|
|
2311
|
+
...(priorDeployment === undefined ? {} : { priorDeployment }),
|
|
2312
|
+
...(workflowWorld === undefined ? {} : { workflowWorld }),
|
|
2045
2313
|
};
|
|
2046
2314
|
}
|
|
2047
2315
|
export async function runEveControlPlane(request, options = {}) {
|
|
@@ -2087,7 +2355,7 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
2087
2355
|
message: status === "indeterminate"
|
|
2088
2356
|
? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
|
|
2089
2357
|
: status === "deployed"
|
|
2090
|
-
? "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."
|
|
2091
2359
|
: "The Eve deployment failed before exact target promotion.",
|
|
2092
2360
|
});
|
|
2093
2361
|
}
|
|
@@ -2215,6 +2483,172 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
2215
2483
|
identity: verified,
|
|
2216
2484
|
};
|
|
2217
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
|
+
}
|
|
2218
2652
|
async function defaultDestroyTargetRead(request) {
|
|
2219
2653
|
const account = await readWranglerJson(["whoami", "--json"]);
|
|
2220
2654
|
const deployments = await readWranglerJson([
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID"];
|
|
1
|
+
export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID"];
|
|
2
2
|
export type EveReservedHostVariable = (typeof EVE_RESERVED_HOST_VARIABLES)[number];
|
|
3
3
|
export declare const EVE_START_COMMAND: readonly ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"];
|
|
4
4
|
type EveRuntimeValueMap = Readonly<Record<string, string>>;
|
|
@@ -9,6 +9,7 @@ export const EVE_RESERVED_HOST_VARIABLES = Object.freeze([
|
|
|
9
9
|
"NODE_ENV",
|
|
10
10
|
"NODE_EXTRA_CA_CERTS",
|
|
11
11
|
"WORKFLOW_LOCAL_BASE_URL",
|
|
12
|
+
"EDEN_WORLD_URL",
|
|
12
13
|
"EDEN_EVE_DEPLOYMENT_ID",
|
|
13
14
|
"EDEN_EVE_GENERATION_ID",
|
|
14
15
|
]);
|
|
@@ -275,7 +276,8 @@ export class EveRuntimeConfig {
|
|
|
275
276
|
createStartEnvironment(hostEnvironment) {
|
|
276
277
|
this.assertUsable();
|
|
277
278
|
for (const name of Object.keys(hostEnvironment)) {
|
|
278
|
-
if (!EVE_RESERVED_HOST_VARIABLES.includes(name)
|
|
279
|
+
if (!EVE_RESERVED_HOST_VARIABLES.includes(name) &&
|
|
280
|
+
name !== "CBOR_NATIVE_ACCELERATION_DISABLED") {
|
|
279
281
|
throw new EveRuntimeConfigError({
|
|
280
282
|
code: "EVE_RUNTIME_INJECTION_INVALID",
|
|
281
283
|
message: `The Eve start environment contains a non-host variable ${name}.`,
|
|
@@ -564,6 +564,12 @@ function runtimeContainerEnvironment(request) {
|
|
|
564
564
|
...(request.publicOrigin === undefined
|
|
565
565
|
? {}
|
|
566
566
|
: { WORKFLOW_LOCAL_BASE_URL: request.publicOrigin }),
|
|
567
|
+
...(request.workflowWorld === "@moinulmoin/eden-world-cloudflare"
|
|
568
|
+
? {
|
|
569
|
+
EDEN_WORLD_URL: `${request.publicOrigin ?? "http://localhost:8080"}/__eden/world/rpc`,
|
|
570
|
+
CBOR_NATIVE_ACCELERATION_DISABLED: "true",
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
567
573
|
};
|
|
568
574
|
}
|
|
569
575
|
function validateHealthPort(port) {
|
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": {
|