@moinulmoin/eden 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +0 -31
- package/README.md +37 -109
- package/dist/eve-control-plane.d.ts +22 -1
- package/dist/eve-control-plane.js +379 -27
- package/dist/eve-packaging.d.ts +7 -0
- package/dist/eve-packaging.js +144 -14
- package/dist/eve-runtime-image.d.ts +7 -0
- package/dist/eve-runtime-image.js +55 -13
- package/dist/eve.d.ts +17 -2
- package/dist/eve.js +60 -26
- package/dist/index.d.ts +7 -169
- package/dist/index.js +49 -6701
- package/package.json +3 -5
|
@@ -9,7 +9,7 @@ import { fileURLToPath, } from "node:url";
|
|
|
9
9
|
import { promisify, } from "node:util";
|
|
10
10
|
import { EveCliError, } from "./eve.js";
|
|
11
11
|
import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
|
|
12
|
-
import { buildEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
|
|
12
|
+
import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
|
|
13
13
|
import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
@@ -29,6 +29,8 @@ export const DEFAULT_EVE_NODE_IMAGE = Object.freeze({
|
|
|
29
29
|
reference: "node:24.17.0-bookworm-slim",
|
|
30
30
|
digest: "sha256:862263c612aa437e3037674b85419622a9d93bff80aa1eee5398dfe686375532",
|
|
31
31
|
});
|
|
32
|
+
/** The single tag Eden pushes to the managed Cloudflare registry per generation. */
|
|
33
|
+
const EVE_REGISTRY_IMAGE_TAG = "candidate";
|
|
32
34
|
function safeText(value) {
|
|
33
35
|
return redactEveRuntimeOutput(value)
|
|
34
36
|
.replace(/(?:^|[\s"'=])(?:Bearer|Token)\s+\S+/giu, "$1[redacted]")
|
|
@@ -73,6 +75,113 @@ function blockedAfterFailure(id, message) {
|
|
|
73
75
|
function allChecksClear(checks) {
|
|
74
76
|
return checks.every((value) => value.status === "passed" || value.status === "skipped");
|
|
75
77
|
}
|
|
78
|
+
/** True when the caller asked for the human progress/summary output. */
|
|
79
|
+
function humanOutput(options) {
|
|
80
|
+
return options.outputFormat === "human";
|
|
81
|
+
}
|
|
82
|
+
function emitLine(options, line) {
|
|
83
|
+
(options.stdout ?? (() => undefined))(redactEveRuntimeOutput(line));
|
|
84
|
+
}
|
|
85
|
+
/** Short stage markers so a multi-minute run shows what it is doing. */
|
|
86
|
+
function eveProgress(options, stage) {
|
|
87
|
+
if (humanOutput(options))
|
|
88
|
+
emitLine(options, `... ${stage}`);
|
|
89
|
+
}
|
|
90
|
+
function eveCheckLines(checks) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (const value of checks) {
|
|
93
|
+
const glyph = value.status === "passed"
|
|
94
|
+
? "✓"
|
|
95
|
+
: value.status === "skipped"
|
|
96
|
+
? "-"
|
|
97
|
+
: "✗";
|
|
98
|
+
lines.push(`${glyph} ${value.id} — ${value.message}`);
|
|
99
|
+
if ((value.status === "failed" || value.status === "blocked") &&
|
|
100
|
+
value.remediation !== undefined) {
|
|
101
|
+
lines.push(` → ${value.remediation}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
function formatEveElapsed(startedAt) {
|
|
107
|
+
const elapsed = (Date.now() - startedAt) / 1000;
|
|
108
|
+
return `${elapsed.toFixed(elapsed < 10 ? 1 : 0)}s`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Human summary lines for a preflight or deploy result: every check with a
|
|
112
|
+
* status glyph, then a compact tail with the outcome.
|
|
113
|
+
*/
|
|
114
|
+
function humanEveResultLines(result, startedAt, eveVersion) {
|
|
115
|
+
const lines = [...eveCheckLines(result.checks)];
|
|
116
|
+
const elapsed = formatEveElapsed(startedAt);
|
|
117
|
+
const commandLabel = result.command === "eve deploy" ? "deploy" : "preflight";
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
if (result.command === "eve deploy") {
|
|
120
|
+
const url = result.deployment?.stableWorkersDevOrigin;
|
|
121
|
+
lines.push("", `✓ deployed ${result.name} (${result.environment})` +
|
|
122
|
+
(eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
|
|
123
|
+
if (url !== undefined)
|
|
124
|
+
lines.push(url);
|
|
125
|
+
lines.push(`done in ${elapsed}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
lines.push("", `✓ ${result.name} (${result.environment}) — preflight passed in ${elapsed}`);
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
const firstFailure = result.checks.find((value) => value.status === "failed" || value.status === "blocked");
|
|
133
|
+
if (result.deployment !== undefined) {
|
|
134
|
+
lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
|
|
135
|
+
? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
|
|
136
|
+
: result.deployment.status === "deployed"
|
|
137
|
+
? "The deployment is live; only local runtime-image cleanup failed, so do not retry the deployment."
|
|
138
|
+
: "The deployment failed before promotion; the exact target was not claimed."}`);
|
|
139
|
+
}
|
|
140
|
+
else if (firstFailure !== undefined) {
|
|
141
|
+
lines.push("", `✗ ${commandLabel} failed — first failing check: ${firstFailure.id}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
lines.push("", `✗ ${commandLabel} failed`);
|
|
145
|
+
}
|
|
146
|
+
lines.push(`stopped after ${elapsed}`);
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
function emitEveResult(options, result, startedAt, eveVersion) {
|
|
150
|
+
if (humanOutput(options)) {
|
|
151
|
+
for (const line of humanEveResultLines(result, startedAt, eveVersion)) {
|
|
152
|
+
emitLine(options, line);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
emitLine(options, JSON.stringify(result));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Human summary lines for a destroy outcome: removed resources, anything
|
|
160
|
+
* retained, and the outcome tail.
|
|
161
|
+
*/
|
|
162
|
+
function humanEveDestroyLines(outcome, startedAt) {
|
|
163
|
+
const lines = [...eveCheckLines(outcome.checks)];
|
|
164
|
+
const elapsed = formatEveElapsed(startedAt);
|
|
165
|
+
if (outcome.ok) {
|
|
166
|
+
lines.push("", outcome.status === "absent"
|
|
167
|
+
? `✓ ${outcome.name} (${outcome.environment}) is already absent — nothing removed`
|
|
168
|
+
: `✓ destroyed ${outcome.name} (${outcome.environment}) in ${elapsed}`, " retained: local deployment records under .eden/eve-deploy");
|
|
169
|
+
return lines;
|
|
170
|
+
}
|
|
171
|
+
const firstFailure = outcome.checks.find((value) => value.status === "failed" || value.status === "blocked");
|
|
172
|
+
lines.push("", `✗ destroy ${outcome.name} ${outcome.status}` +
|
|
173
|
+
(firstFailure === undefined ? "" : ` — first failing check: ${firstFailure.id}`));
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
function emitEveDestroyOutcome(options, outcome, startedAt) {
|
|
177
|
+
if (humanOutput(options)) {
|
|
178
|
+
for (const line of humanEveDestroyLines(outcome, startedAt)) {
|
|
179
|
+
emitLine(options, line);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
emitLine(options, JSON.stringify(outcome));
|
|
184
|
+
}
|
|
76
185
|
function runtimeConfigExclusion(config) {
|
|
77
186
|
if (config === undefined)
|
|
78
187
|
return undefined;
|
|
@@ -1018,12 +1127,13 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1018
1127
|
}
|
|
1019
1128
|
}
|
|
1020
1129
|
else {
|
|
1130
|
+
eveProgress(options, "pushing container image to the Cloudflare registry");
|
|
1021
1131
|
const localImageReference = runtimeEvidence.imageReference;
|
|
1022
1132
|
if (localImageReference === undefined || localImageReference.length === 0) {
|
|
1023
1133
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
|
|
1024
1134
|
}
|
|
1025
1135
|
const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
|
|
1026
|
-
const imageTag = `${imageRepository}
|
|
1136
|
+
const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
1027
1137
|
const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
|
|
1028
1138
|
const imagePublication = await (options.publishImage ??
|
|
1029
1139
|
defaultImagePublicationRunner)({
|
|
@@ -1062,6 +1172,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1062
1172
|
}
|
|
1063
1173
|
let runtimeInjection;
|
|
1064
1174
|
if (collected.runtimeConfig !== undefined) {
|
|
1175
|
+
eveProgress(options, "registering runtime variables");
|
|
1065
1176
|
try {
|
|
1066
1177
|
runtimeInjection = await prepareEveRuntimeInjection(collected.runtimeConfig, {
|
|
1067
1178
|
mode: "deploy",
|
|
@@ -1143,6 +1254,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1143
1254
|
};
|
|
1144
1255
|
let publication;
|
|
1145
1256
|
try {
|
|
1257
|
+
eveProgress(options, "publishing Worker and Container application");
|
|
1146
1258
|
publication = await (options.publish ?? defaultPublicationRunner())(publicationRequest);
|
|
1147
1259
|
}
|
|
1148
1260
|
catch {
|
|
@@ -1217,6 +1329,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1217
1329
|
}
|
|
1218
1330
|
let health;
|
|
1219
1331
|
try {
|
|
1332
|
+
eveProgress(options, `verifying health at ${stableWorkersDevOrigin}`);
|
|
1220
1333
|
health = await (options.health ?? defaultHealthRunner())({
|
|
1221
1334
|
identity,
|
|
1222
1335
|
origin: stableWorkersDevOrigin,
|
|
@@ -1341,15 +1454,44 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1341
1454
|
});
|
|
1342
1455
|
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), targetKey, candidate.generationRoot);
|
|
1343
1456
|
await options.afterPromotion?.();
|
|
1457
|
+
let runtimeImageCleanup;
|
|
1458
|
+
if (runtimeEvidence.cleanup.imageRetained) {
|
|
1459
|
+
if (options.retainRuntimeImage === true) {
|
|
1460
|
+
runtimeImageCleanup = check("VAL-CROSS-004", "skipped", "The exact local runtime image was intentionally retained by the programmatic caller.");
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
let discarded = false;
|
|
1464
|
+
try {
|
|
1465
|
+
discarded = await (options.discardRuntimeImage ??
|
|
1466
|
+
discardEveRuntimeImage)({
|
|
1467
|
+
imageId: imageDigest,
|
|
1468
|
+
generationId: candidate.generationId,
|
|
1469
|
+
generationRoot: candidate.generationRoot,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
discarded = false;
|
|
1474
|
+
}
|
|
1475
|
+
runtimeImageCleanup = discarded
|
|
1476
|
+
? check("VAL-CROSS-004", "passed", "The exact local runtime image and its publication tags were removed after the healthy deployment was promoted.")
|
|
1477
|
+
: 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.`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1344
1480
|
return {
|
|
1345
1481
|
...collected.result,
|
|
1346
|
-
ok:
|
|
1482
|
+
ok: runtimeImageCleanup?.status !== "failed",
|
|
1483
|
+
...(runtimeImageCleanup === undefined
|
|
1484
|
+
? {}
|
|
1485
|
+
: { checks: [...collected.result.checks, runtimeImageCleanup] }),
|
|
1347
1486
|
deployment: deploymentMetadata(identity, targetKey, "deployed"),
|
|
1348
1487
|
};
|
|
1349
1488
|
}
|
|
1350
1489
|
async function collectEvePreflight(request, options) {
|
|
1351
1490
|
let runtimeConfig;
|
|
1352
1491
|
const checks = [];
|
|
1492
|
+
if (request.envFile !== undefined) {
|
|
1493
|
+
eveProgress(options, "validating environment file");
|
|
1494
|
+
}
|
|
1353
1495
|
try {
|
|
1354
1496
|
if (request.envFile === undefined) {
|
|
1355
1497
|
checks.push(check("VAL-SEC-001", "skipped", "No explicit --env-file was supplied; no implicit project environment source was read."), check("VAL-SEC-002", "skipped", "No explicit runtime names were supplied; reserved host-name validation was not needed."));
|
|
@@ -1378,6 +1520,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1378
1520
|
runtimeConfig,
|
|
1379
1521
|
};
|
|
1380
1522
|
}
|
|
1523
|
+
eveProgress(options, "packaging project — snapshot, frozen install, eve build");
|
|
1381
1524
|
const nodeImage = options.nodeImage ?? DEFAULT_EVE_NODE_IMAGE;
|
|
1382
1525
|
const artifactRoot = options.artifactRoot ??
|
|
1383
1526
|
join(request.projectRoot, ".eden", "eve-deploy", "generations", `preflight-${randomUUID()}`);
|
|
@@ -1474,6 +1617,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1474
1617
|
let cloudflare;
|
|
1475
1618
|
let publicOrigin;
|
|
1476
1619
|
if (request.command === "deploy") {
|
|
1620
|
+
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1477
1621
|
try {
|
|
1478
1622
|
cloudflare = await readEveCloudflareTarget({
|
|
1479
1623
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
@@ -1547,6 +1691,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1547
1691
|
let runtimeEvidence;
|
|
1548
1692
|
let runtimeInjection;
|
|
1549
1693
|
if (hostRequirements !== undefined && hostRequirementsValid) {
|
|
1694
|
+
eveProgress(options, "building linux/amd64 runtime image, booting eve, checking health");
|
|
1550
1695
|
try {
|
|
1551
1696
|
if (runtimeConfig !== undefined && request.command === "preflight") {
|
|
1552
1697
|
runtimeInjection = await prepareEveRuntimeInjection(runtimeConfig, {
|
|
@@ -1620,6 +1765,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1620
1765
|
};
|
|
1621
1766
|
}
|
|
1622
1767
|
if (cloudflare === undefined) {
|
|
1768
|
+
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1623
1769
|
try {
|
|
1624
1770
|
cloudflare = await readEveCloudflareTarget({
|
|
1625
1771
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
@@ -1665,6 +1811,10 @@ async function collectEvePreflight(request, options) {
|
|
|
1665
1811
|
};
|
|
1666
1812
|
}
|
|
1667
1813
|
export async function runEveControlPlane(request, options = {}) {
|
|
1814
|
+
const startedAt = Date.now();
|
|
1815
|
+
if (humanOutput(options)) {
|
|
1816
|
+
emitLine(options, `eden ${request.command} — ${request.name} (${request.environment})`);
|
|
1817
|
+
}
|
|
1668
1818
|
const lock = request.command === "deploy"
|
|
1669
1819
|
? await acquireEveTargetLock(request.projectRoot, request.environment, request.name)
|
|
1670
1820
|
: undefined;
|
|
@@ -1674,8 +1824,9 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
1674
1824
|
let lockReleased = true;
|
|
1675
1825
|
try {
|
|
1676
1826
|
collected = await collectEvePreflight(request, options);
|
|
1827
|
+
const eveVersion = collected.candidate?.eveVersion;
|
|
1677
1828
|
if (!collected.result.ok) {
|
|
1678
|
-
(options
|
|
1829
|
+
emitEveResult(options, collected.result, startedAt, eveVersion);
|
|
1679
1830
|
throw new EveCliError({
|
|
1680
1831
|
code: request.command === "deploy"
|
|
1681
1832
|
? "EVE_DEPLOY_CHECKS_FAILED"
|
|
@@ -1686,20 +1837,24 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
1686
1837
|
});
|
|
1687
1838
|
}
|
|
1688
1839
|
if (request.command === "preflight") {
|
|
1689
|
-
(options
|
|
1840
|
+
emitEveResult(options, collected.result, startedAt, eveVersion);
|
|
1690
1841
|
}
|
|
1691
1842
|
else {
|
|
1692
1843
|
const deploymentResult = await runEveDeployment(request, options, collected);
|
|
1693
|
-
(options
|
|
1844
|
+
emitEveResult(options, deploymentResult, startedAt, eveVersion);
|
|
1694
1845
|
if (!deploymentResult.ok) {
|
|
1695
1846
|
const status = deploymentResult.deployment?.status;
|
|
1696
1847
|
throw new EveCliError({
|
|
1697
1848
|
code: status === "indeterminate"
|
|
1698
1849
|
? "DEPLOY_INDETERMINATE"
|
|
1699
|
-
: "
|
|
1850
|
+
: status === "deployed"
|
|
1851
|
+
? "EVE_RUNTIME_IMAGE_CLEANUP_FAILED"
|
|
1852
|
+
: "EVE_DEPLOYMENT_FAILED",
|
|
1700
1853
|
message: status === "indeterminate"
|
|
1701
1854
|
? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
|
|
1702
|
-
:
|
|
1855
|
+
: status === "deployed"
|
|
1856
|
+
? "The Eve deployment is healthy and promoted, but exact local runtime-image cleanup failed; do not retry the deployment."
|
|
1857
|
+
: "The Eve deployment failed before exact target promotion.",
|
|
1703
1858
|
});
|
|
1704
1859
|
}
|
|
1705
1860
|
}
|
|
@@ -1802,9 +1957,28 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
1802
1957
|
if (identity.environment !== environment || identity.name !== name) {
|
|
1803
1958
|
return undefined;
|
|
1804
1959
|
}
|
|
1960
|
+
const verified = {
|
|
1961
|
+
workerName: identity.workerName,
|
|
1962
|
+
containerApplicationName: identity.containerApplicationName,
|
|
1963
|
+
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1964
|
+
accountId: identity.accountId,
|
|
1965
|
+
environment,
|
|
1966
|
+
name,
|
|
1967
|
+
...(typeof identity.projectId === "string" && identity.projectId.length > 0
|
|
1968
|
+
? { projectId: identity.projectId }
|
|
1969
|
+
: {}),
|
|
1970
|
+
...(typeof identity.generationId === "string" &&
|
|
1971
|
+
identity.generationId.length > 0
|
|
1972
|
+
? { generationId: identity.generationId }
|
|
1973
|
+
: {}),
|
|
1974
|
+
...(typeof identity.containerImage === "string" &&
|
|
1975
|
+
identity.containerImage.length > 0
|
|
1976
|
+
? { containerImage: identity.containerImage }
|
|
1977
|
+
: {}),
|
|
1978
|
+
};
|
|
1805
1979
|
return {
|
|
1806
1980
|
recordPath,
|
|
1807
|
-
identity:
|
|
1981
|
+
identity: verified,
|
|
1808
1982
|
};
|
|
1809
1983
|
}
|
|
1810
1984
|
async function defaultDestroyTargetRead(request) {
|
|
@@ -1828,14 +2002,57 @@ async function defaultDestroyTargetRead(request) {
|
|
|
1828
2002
|
(exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
|
|
1829
2003
|
const entries = exactContainerEntries(containers.value, request.containerApplicationName);
|
|
1830
2004
|
const firstContainer = entries === undefined ? undefined : entries[0];
|
|
2005
|
+
const ownedImages = request.registryImages;
|
|
2006
|
+
let registryImagesPresent;
|
|
2007
|
+
if (ownedImages !== undefined && ownedImages.length > 0) {
|
|
2008
|
+
const images = await readWranglerJson([
|
|
2009
|
+
"containers",
|
|
2010
|
+
"images",
|
|
2011
|
+
"list",
|
|
2012
|
+
"--json",
|
|
2013
|
+
]);
|
|
2014
|
+
const present = registryImageNames(images.value);
|
|
2015
|
+
if (!images.failed && present !== undefined) {
|
|
2016
|
+
registryImagesPresent = ownedImages.filter((image) => present.has(image));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
1831
2019
|
return {
|
|
1832
2020
|
workerExists,
|
|
1833
2021
|
...(firstContainer === undefined
|
|
1834
2022
|
? {}
|
|
1835
2023
|
: { containerApplicationId: firstContainer.id }),
|
|
1836
2024
|
...(accountId === undefined ? {} : { accountId }),
|
|
2025
|
+
...(registryImagesPresent === undefined
|
|
2026
|
+
? {}
|
|
2027
|
+
: { registryImagesPresent }),
|
|
1837
2028
|
};
|
|
1838
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* `wrangler containers images list --json` reports `[{name, tags}]` entries
|
|
2032
|
+
* with the account prefix already stripped from the repository name. The set
|
|
2033
|
+
* carries `repository:tag` refs for the owned tags Eden publishes.
|
|
2034
|
+
*/
|
|
2035
|
+
function registryImageNames(value) {
|
|
2036
|
+
const collection = jsonCollection(value);
|
|
2037
|
+
if (collection === undefined)
|
|
2038
|
+
return undefined;
|
|
2039
|
+
const names = new Set();
|
|
2040
|
+
for (const entry of collection) {
|
|
2041
|
+
if (typeof entry !== "object" || entry === null || !("name" in entry)) {
|
|
2042
|
+
return undefined;
|
|
2043
|
+
}
|
|
2044
|
+
const name = entry.name;
|
|
2045
|
+
const tags = "tags" in entry ? entry.tags : undefined;
|
|
2046
|
+
if (typeof name !== "string" || !Array.isArray(tags))
|
|
2047
|
+
return undefined;
|
|
2048
|
+
for (const tag of tags) {
|
|
2049
|
+
if (typeof tag !== "string")
|
|
2050
|
+
return undefined;
|
|
2051
|
+
names.add(`${name}:${tag}`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return names;
|
|
2055
|
+
}
|
|
1839
2056
|
async function defaultWorkerDelete(request) {
|
|
1840
2057
|
const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
|
|
1841
2058
|
if (result.exitCode === 0)
|
|
@@ -1854,19 +2071,114 @@ async function defaultContainerDelete(request) {
|
|
|
1854
2071
|
}
|
|
1855
2072
|
return "indeterminate";
|
|
1856
2073
|
}
|
|
1857
|
-
async function
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2074
|
+
async function defaultRegistryImageDelete(request) {
|
|
2075
|
+
const result = await runWranglerWithInput(["containers", "images", "delete", request.image, "--skip-confirmation"], process.cwd());
|
|
2076
|
+
if (result.exitCode === 0)
|
|
2077
|
+
return "deleted";
|
|
2078
|
+
if (/(?:does not exist|not found|404)/iu.test(result.stderr)) {
|
|
2079
|
+
return "absent";
|
|
2080
|
+
}
|
|
2081
|
+
return "indeterminate";
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* The exact `<repository>:${EVE_REGISTRY_IMAGE_TAG}` ref this generation's
|
|
2085
|
+
* deployment record proves Eden pushed, or undefined. Ownership holds only
|
|
2086
|
+
* when the recorded digest reference points at the repository derived from
|
|
2087
|
+
* the same identity that produced it; a digest-bearing reference to any other
|
|
2088
|
+
* repository was published outside Eden's push and is never owned here.
|
|
2089
|
+
*/
|
|
2090
|
+
function ownedRegistryImageRef(identity, environment, name, accountId) {
|
|
2091
|
+
if (identity.projectId === undefined ||
|
|
2092
|
+
identity.generationId === undefined ||
|
|
2093
|
+
identity.containerImage === undefined ||
|
|
2094
|
+
identity.accountId !== accountId) {
|
|
2095
|
+
return undefined;
|
|
2096
|
+
}
|
|
2097
|
+
const repository = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-${identity.generationId}`;
|
|
2098
|
+
const expected = `registry.cloudflare.com/${identity.accountId}/${repository}@`;
|
|
2099
|
+
if (!identity.containerImage.startsWith(expected) ||
|
|
2100
|
+
imageDigestFromReference(identity.containerImage) === undefined) {
|
|
2101
|
+
return undefined;
|
|
2102
|
+
}
|
|
2103
|
+
return `${repository}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* The exact image ref an indeterminate deploy attempt provably pushed. The
|
|
2107
|
+
* retained attempt evidence names the target's repository; the account and
|
|
2108
|
+
* target key must match the verified destroy record.
|
|
2109
|
+
*/
|
|
2110
|
+
async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
|
|
2111
|
+
const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
|
|
2112
|
+
if (raw === undefined)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
let parsed;
|
|
2115
|
+
try {
|
|
2116
|
+
parsed = JSON.parse(raw);
|
|
2117
|
+
}
|
|
2118
|
+
catch {
|
|
2119
|
+
return undefined;
|
|
2120
|
+
}
|
|
2121
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2122
|
+
return undefined;
|
|
2123
|
+
const prefix = `eden-eve-${targetKey}-`;
|
|
2124
|
+
if ("imageRepository" in parsed &&
|
|
2125
|
+
typeof parsed.imageRepository === "string" &&
|
|
2126
|
+
parsed.imageRepository.startsWith(prefix) &&
|
|
2127
|
+
(!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
|
|
2128
|
+
"accountId" in parsed &&
|
|
2129
|
+
parsed.accountId === accountId) {
|
|
2130
|
+
return `${parsed.imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
2131
|
+
}
|
|
2132
|
+
if ("imageReference" in parsed &&
|
|
2133
|
+
typeof parsed.imageReference === "string" &&
|
|
2134
|
+
imageDigestFromReference(parsed.imageReference) !== undefined) {
|
|
2135
|
+
const repository = parsed.imageReference.split("@")[0];
|
|
2136
|
+
const expected = `registry.cloudflare.com/${accountId}/${prefix}`;
|
|
2137
|
+
if (repository !== undefined &&
|
|
2138
|
+
repository.startsWith(expected) &&
|
|
2139
|
+
(!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
|
|
2140
|
+
return `${repository.slice(`registry.cloudflare.com/${accountId}/`.length)}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return undefined;
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Every registry image tag this exact target provably owns: the recorded
|
|
2147
|
+
* digest references in matching deployment records plus the repository
|
|
2148
|
+
* evidence retained by indeterminate pushes. Never derived by prefix-matching
|
|
2149
|
+
* the registry; each ref is computed from the immutable generation records.
|
|
2150
|
+
*/
|
|
2151
|
+
async function collectEveDestroyRegistryImages(projectRoot, environment, name, record) {
|
|
2152
|
+
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2153
|
+
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2154
|
+
const images = new Set();
|
|
2155
|
+
const targetKey = record.projectId === undefined
|
|
2156
|
+
? undefined
|
|
2157
|
+
: stableDeploymentKey(record.projectId, record.accountId, environment, name);
|
|
2158
|
+
for (const entry of entries) {
|
|
2159
|
+
const generationRoot = join(generationsRoot, entry);
|
|
2160
|
+
const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
|
|
2161
|
+
const deployedImage = parsed === undefined
|
|
2162
|
+
? undefined
|
|
2163
|
+
: ownedRegistryImageRef(parsed.identity, environment, name, record.accountId);
|
|
2164
|
+
if (deployedImage !== undefined)
|
|
2165
|
+
images.add(deployedImage);
|
|
2166
|
+
if (targetKey === undefined)
|
|
2167
|
+
continue;
|
|
2168
|
+
const attemptImage = await attemptRegistryImageRef(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
|
|
2169
|
+
if (attemptImage !== undefined)
|
|
2170
|
+
images.add(attemptImage);
|
|
2171
|
+
}
|
|
2172
|
+
return [...images];
|
|
1865
2173
|
}
|
|
1866
2174
|
export async function runEveDestroy(request, options = {}) {
|
|
2175
|
+
const startedAt = Date.now();
|
|
2176
|
+
if (humanOutput(options)) {
|
|
2177
|
+
emitLine(options, `eden destroy — ${request.name} (${request.environment})`);
|
|
2178
|
+
}
|
|
1867
2179
|
const checks = [];
|
|
1868
2180
|
const emit = (outcome) => {
|
|
1869
|
-
(options
|
|
2181
|
+
emitEveDestroyOutcome(options, outcome, startedAt);
|
|
1870
2182
|
};
|
|
1871
2183
|
const fail = (code, message) => {
|
|
1872
2184
|
throw new EveCliError({ code, message, source: "eve destroy" });
|
|
@@ -1909,15 +2221,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1909
2221
|
fail("EVE_DESTROY_RECORD_MISMATCH", "The immutable deployment record does not match the selected target.");
|
|
1910
2222
|
}
|
|
1911
2223
|
checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
|
|
1912
|
-
|
|
1913
|
-
const
|
|
2224
|
+
eveProgress(options, "verifying the immutable ownership record");
|
|
2225
|
+
const ownedRegistryImages = await collectEveDestroyRegistryImages(request.projectRoot, request.environment, request.name, record.identity);
|
|
2226
|
+
const readTarget = () => Promise.resolve(reader({
|
|
1914
2227
|
workerName: expectedWorker,
|
|
1915
2228
|
containerApplicationName: expectedContainer,
|
|
1916
|
-
|
|
2229
|
+
registryImages: ownedRegistryImages,
|
|
2230
|
+
}));
|
|
2231
|
+
const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
|
|
2232
|
+
eveProgress(options, "reading the exact remote inventory");
|
|
2233
|
+
const before = await readTarget();
|
|
1917
2234
|
if (before.accountId !== record.identity.accountId) {
|
|
1918
2235
|
fail("EVE_DESTROY_OWNERSHIP_UNPROVEN", "The authenticated account does not own the recorded deployment account.");
|
|
1919
2236
|
}
|
|
1920
|
-
if (!before.workerExists &&
|
|
2237
|
+
if (!before.workerExists &&
|
|
2238
|
+
before.containerApplicationId === undefined &&
|
|
2239
|
+
(ownedRegistryImages.length === 0 ||
|
|
2240
|
+
(before.registryImagesPresent !== undefined &&
|
|
2241
|
+
before.registryImagesPresent.length === 0))) {
|
|
1921
2242
|
emit({
|
|
1922
2243
|
ok: true,
|
|
1923
2244
|
status: "absent",
|
|
@@ -1932,6 +2253,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1932
2253
|
}
|
|
1933
2254
|
const deleteWorker = options.deleteWorker ?? defaultWorkerDelete;
|
|
1934
2255
|
if (before.workerExists) {
|
|
2256
|
+
eveProgress(options, `deleting Worker ${expectedWorker}`);
|
|
1935
2257
|
const workerResult = await deleteWorker({ name: expectedWorker });
|
|
1936
2258
|
if (workerResult === "indeterminate") {
|
|
1937
2259
|
emit({
|
|
@@ -1952,6 +2274,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1952
2274
|
}
|
|
1953
2275
|
const deleteContainer = options.deleteContainer ?? defaultContainerDelete;
|
|
1954
2276
|
if (before.containerApplicationId !== undefined) {
|
|
2277
|
+
eveProgress(options, `deleting Container application ${expectedContainer}`);
|
|
1955
2278
|
const containerResult = await deleteContainer({
|
|
1956
2279
|
applicationId: before.containerApplicationId,
|
|
1957
2280
|
});
|
|
@@ -1972,11 +2295,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1972
2295
|
? "The exact owned Container application was deleted."
|
|
1973
2296
|
: "The exact Container application was already absent."));
|
|
1974
2297
|
}
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2298
|
+
const imageDeletes = new Map();
|
|
2299
|
+
if (ownedRegistryImages.length > 0) {
|
|
2300
|
+
const deleteRegistryImage = options.deleteRegistryImage ?? defaultRegistryImageDelete;
|
|
2301
|
+
const deletable = before.registryImagesPresent === undefined
|
|
2302
|
+
? ownedRegistryImages
|
|
2303
|
+
: ownedRegistryImages.filter((image) => before.registryImagesPresent?.includes(image) === true);
|
|
2304
|
+
if (deletable.length > 0) {
|
|
2305
|
+
eveProgress(options, `deleting ${deletable.length} registry image${deletable.length === 1 ? "" : "s"}`);
|
|
2306
|
+
}
|
|
2307
|
+
for (const image of deletable) {
|
|
2308
|
+
imageDeletes.set(image, await deleteRegistryImage({ image }));
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
eveProgress(options, "verifying the exact target is absent");
|
|
2312
|
+
const after = await readTarget();
|
|
2313
|
+
if (after.workerExists || after.containerApplicationId !== undefined) {
|
|
1980
2314
|
emit({
|
|
1981
2315
|
ok: false,
|
|
1982
2316
|
status: "failed",
|
|
@@ -1989,6 +2323,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1989
2323
|
});
|
|
1990
2324
|
fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
|
|
1991
2325
|
}
|
|
2326
|
+
const retainedImages = ownedRegistryImages.filter((image) => after.registryImagesPresent === undefined
|
|
2327
|
+
? imageDeletes.get(image) === "indeterminate"
|
|
2328
|
+
: after.registryImagesPresent.includes(image));
|
|
2329
|
+
if (ownedRegistryImages.length > 0) {
|
|
2330
|
+
checks.push(retainedImages.length === 0
|
|
2331
|
+
? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
|
|
2332
|
+
: check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${retainedImages.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed image refs with wrangler containers images delete."));
|
|
2333
|
+
}
|
|
2334
|
+
if (retainedImages.length > 0) {
|
|
2335
|
+
emit({
|
|
2336
|
+
ok: false,
|
|
2337
|
+
status: "failed",
|
|
2338
|
+
environment: request.environment,
|
|
2339
|
+
name: request.name,
|
|
2340
|
+
checks,
|
|
2341
|
+
});
|
|
2342
|
+
fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${retainedImages.join(", ")}.`);
|
|
2343
|
+
}
|
|
1992
2344
|
checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
|
|
1993
2345
|
const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
|
|
1994
2346
|
await rm(currentPath, { force: true });
|
package/dist/eve-packaging.d.ts
CHANGED
|
@@ -252,6 +252,13 @@ export declare class EvePackagingError extends Error {
|
|
|
252
252
|
readonly remediation: string;
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* The authored Eve project source the runtime stage must carry so `eve start`
|
|
257
|
+
* can resolve the project context (agent/, agents/ workspaces, flat layouts,
|
|
258
|
+
* and the manifest). Returns validated top-level snapshot entries with every
|
|
259
|
+
* capture-time exclusion still applied; generated roots are never source.
|
|
260
|
+
*/
|
|
261
|
+
export declare function eveSnapshotSourceEntries(snapshotRoot: string): Promise<readonly string[]>;
|
|
255
262
|
export declare function buildEveProjectSnapshot(options: EveProjectSnapshotOptions): Promise<EveProjectPackagingResult>;
|
|
256
263
|
export declare function revalidateEveProjectCandidateInputs(candidate: EveProjectBuildCandidate, runtimeConfig?: EveRuntimeConfigExclusion): Promise<void>;
|
|
257
264
|
export declare function createDockerEveProjectBuilder(options: {
|