@moinulmoin/eden 0.1.5 → 0.2.1
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 +19 -0
- package/dist/eve-control-plane.js +345 -23
- package/dist/eve-packaging.d.ts +7 -0
- package/dist/eve-packaging.js +144 -14
- package/dist/eve-runtime-config.d.ts +1 -1
- package/dist/eve-runtime-config.js +1 -0
- package/dist/eve-runtime-image.js +15 -13
- package/dist/eve.d.ts +17 -2
- package/dist/eve.js +60 -26
- package/dist/index.d.ts +4 -166
- package/dist/index.js +48 -6700
- package/package.json +3 -5
|
@@ -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,
|
|
@@ -1376,6 +1489,9 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1376
1489
|
async function collectEvePreflight(request, options) {
|
|
1377
1490
|
let runtimeConfig;
|
|
1378
1491
|
const checks = [];
|
|
1492
|
+
if (request.envFile !== undefined) {
|
|
1493
|
+
eveProgress(options, "validating environment file");
|
|
1494
|
+
}
|
|
1379
1495
|
try {
|
|
1380
1496
|
if (request.envFile === undefined) {
|
|
1381
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."));
|
|
@@ -1404,6 +1520,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1404
1520
|
runtimeConfig,
|
|
1405
1521
|
};
|
|
1406
1522
|
}
|
|
1523
|
+
eveProgress(options, "packaging project — snapshot, frozen install, eve build");
|
|
1407
1524
|
const nodeImage = options.nodeImage ?? DEFAULT_EVE_NODE_IMAGE;
|
|
1408
1525
|
const artifactRoot = options.artifactRoot ??
|
|
1409
1526
|
join(request.projectRoot, ".eden", "eve-deploy", "generations", `preflight-${randomUUID()}`);
|
|
@@ -1500,6 +1617,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1500
1617
|
let cloudflare;
|
|
1501
1618
|
let publicOrigin;
|
|
1502
1619
|
if (request.command === "deploy") {
|
|
1620
|
+
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1503
1621
|
try {
|
|
1504
1622
|
cloudflare = await readEveCloudflareTarget({
|
|
1505
1623
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
@@ -1573,6 +1691,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1573
1691
|
let runtimeEvidence;
|
|
1574
1692
|
let runtimeInjection;
|
|
1575
1693
|
if (hostRequirements !== undefined && hostRequirementsValid) {
|
|
1694
|
+
eveProgress(options, "building linux/amd64 runtime image, booting eve, checking health");
|
|
1576
1695
|
try {
|
|
1577
1696
|
if (runtimeConfig !== undefined && request.command === "preflight") {
|
|
1578
1697
|
runtimeInjection = await prepareEveRuntimeInjection(runtimeConfig, {
|
|
@@ -1646,6 +1765,7 @@ async function collectEvePreflight(request, options) {
|
|
|
1646
1765
|
};
|
|
1647
1766
|
}
|
|
1648
1767
|
if (cloudflare === undefined) {
|
|
1768
|
+
eveProgress(options, "checking Cloudflare access and exact target");
|
|
1649
1769
|
try {
|
|
1650
1770
|
cloudflare = await readEveCloudflareTarget({
|
|
1651
1771
|
projectId: packaging.project?.projectId ?? "unknown-project",
|
|
@@ -1691,6 +1811,10 @@ async function collectEvePreflight(request, options) {
|
|
|
1691
1811
|
};
|
|
1692
1812
|
}
|
|
1693
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
|
+
}
|
|
1694
1818
|
const lock = request.command === "deploy"
|
|
1695
1819
|
? await acquireEveTargetLock(request.projectRoot, request.environment, request.name)
|
|
1696
1820
|
: undefined;
|
|
@@ -1700,8 +1824,9 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
1700
1824
|
let lockReleased = true;
|
|
1701
1825
|
try {
|
|
1702
1826
|
collected = await collectEvePreflight(request, options);
|
|
1827
|
+
const eveVersion = collected.candidate?.eveVersion;
|
|
1703
1828
|
if (!collected.result.ok) {
|
|
1704
|
-
(options
|
|
1829
|
+
emitEveResult(options, collected.result, startedAt, eveVersion);
|
|
1705
1830
|
throw new EveCliError({
|
|
1706
1831
|
code: request.command === "deploy"
|
|
1707
1832
|
? "EVE_DEPLOY_CHECKS_FAILED"
|
|
@@ -1712,11 +1837,11 @@ export async function runEveControlPlane(request, options = {}) {
|
|
|
1712
1837
|
});
|
|
1713
1838
|
}
|
|
1714
1839
|
if (request.command === "preflight") {
|
|
1715
|
-
(options
|
|
1840
|
+
emitEveResult(options, collected.result, startedAt, eveVersion);
|
|
1716
1841
|
}
|
|
1717
1842
|
else {
|
|
1718
1843
|
const deploymentResult = await runEveDeployment(request, options, collected);
|
|
1719
|
-
(options
|
|
1844
|
+
emitEveResult(options, deploymentResult, startedAt, eveVersion);
|
|
1720
1845
|
if (!deploymentResult.ok) {
|
|
1721
1846
|
const status = deploymentResult.deployment?.status;
|
|
1722
1847
|
throw new EveCliError({
|
|
@@ -1832,9 +1957,28 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
1832
1957
|
if (identity.environment !== environment || identity.name !== name) {
|
|
1833
1958
|
return undefined;
|
|
1834
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
|
+
};
|
|
1835
1979
|
return {
|
|
1836
1980
|
recordPath,
|
|
1837
|
-
identity:
|
|
1981
|
+
identity: verified,
|
|
1838
1982
|
};
|
|
1839
1983
|
}
|
|
1840
1984
|
async function defaultDestroyTargetRead(request) {
|
|
@@ -1858,14 +2002,57 @@ async function defaultDestroyTargetRead(request) {
|
|
|
1858
2002
|
(exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
|
|
1859
2003
|
const entries = exactContainerEntries(containers.value, request.containerApplicationName);
|
|
1860
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
|
+
}
|
|
1861
2019
|
return {
|
|
1862
2020
|
workerExists,
|
|
1863
2021
|
...(firstContainer === undefined
|
|
1864
2022
|
? {}
|
|
1865
2023
|
: { containerApplicationId: firstContainer.id }),
|
|
1866
2024
|
...(accountId === undefined ? {} : { accountId }),
|
|
2025
|
+
...(registryImagesPresent === undefined
|
|
2026
|
+
? {}
|
|
2027
|
+
: { registryImagesPresent }),
|
|
1867
2028
|
};
|
|
1868
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
|
+
}
|
|
1869
2056
|
async function defaultWorkerDelete(request) {
|
|
1870
2057
|
const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
|
|
1871
2058
|
if (result.exitCode === 0)
|
|
@@ -1884,19 +2071,114 @@ async function defaultContainerDelete(request) {
|
|
|
1884
2071
|
}
|
|
1885
2072
|
return "indeterminate";
|
|
1886
2073
|
}
|
|
1887
|
-
async function
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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];
|
|
1895
2173
|
}
|
|
1896
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
|
+
}
|
|
1897
2179
|
const checks = [];
|
|
1898
2180
|
const emit = (outcome) => {
|
|
1899
|
-
(options
|
|
2181
|
+
emitEveDestroyOutcome(options, outcome, startedAt);
|
|
1900
2182
|
};
|
|
1901
2183
|
const fail = (code, message) => {
|
|
1902
2184
|
throw new EveCliError({ code, message, source: "eve destroy" });
|
|
@@ -1939,15 +2221,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1939
2221
|
fail("EVE_DESTROY_RECORD_MISMATCH", "The immutable deployment record does not match the selected target.");
|
|
1940
2222
|
}
|
|
1941
2223
|
checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
|
|
1942
|
-
|
|
1943
|
-
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({
|
|
1944
2227
|
workerName: expectedWorker,
|
|
1945
2228
|
containerApplicationName: expectedContainer,
|
|
1946
|
-
|
|
2229
|
+
registryImages: ownedRegistryImages,
|
|
2230
|
+
}));
|
|
2231
|
+
const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
|
|
2232
|
+
eveProgress(options, "reading the exact remote inventory");
|
|
2233
|
+
const before = await readTarget();
|
|
1947
2234
|
if (before.accountId !== record.identity.accountId) {
|
|
1948
2235
|
fail("EVE_DESTROY_OWNERSHIP_UNPROVEN", "The authenticated account does not own the recorded deployment account.");
|
|
1949
2236
|
}
|
|
1950
|
-
if (!before.workerExists &&
|
|
2237
|
+
if (!before.workerExists &&
|
|
2238
|
+
before.containerApplicationId === undefined &&
|
|
2239
|
+
(ownedRegistryImages.length === 0 ||
|
|
2240
|
+
(before.registryImagesPresent !== undefined &&
|
|
2241
|
+
before.registryImagesPresent.length === 0))) {
|
|
1951
2242
|
emit({
|
|
1952
2243
|
ok: true,
|
|
1953
2244
|
status: "absent",
|
|
@@ -1962,6 +2253,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1962
2253
|
}
|
|
1963
2254
|
const deleteWorker = options.deleteWorker ?? defaultWorkerDelete;
|
|
1964
2255
|
if (before.workerExists) {
|
|
2256
|
+
eveProgress(options, `deleting Worker ${expectedWorker}`);
|
|
1965
2257
|
const workerResult = await deleteWorker({ name: expectedWorker });
|
|
1966
2258
|
if (workerResult === "indeterminate") {
|
|
1967
2259
|
emit({
|
|
@@ -1982,6 +2274,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
1982
2274
|
}
|
|
1983
2275
|
const deleteContainer = options.deleteContainer ?? defaultContainerDelete;
|
|
1984
2276
|
if (before.containerApplicationId !== undefined) {
|
|
2277
|
+
eveProgress(options, `deleting Container application ${expectedContainer}`);
|
|
1985
2278
|
const containerResult = await deleteContainer({
|
|
1986
2279
|
applicationId: before.containerApplicationId,
|
|
1987
2280
|
});
|
|
@@ -2002,11 +2295,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2002
2295
|
? "The exact owned Container application was deleted."
|
|
2003
2296
|
: "The exact Container application was already absent."));
|
|
2004
2297
|
}
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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) {
|
|
2010
2314
|
emit({
|
|
2011
2315
|
ok: false,
|
|
2012
2316
|
status: "failed",
|
|
@@ -2019,6 +2323,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2019
2323
|
});
|
|
2020
2324
|
fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
|
|
2021
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
|
+
}
|
|
2022
2344
|
checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
|
|
2023
2345
|
const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
|
|
2024
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: {
|