@void/deploy-cloudflare 0.21.1 → 0.21.3
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/dist/index.d.mts +5 -2
- package/dist/index.mjs +93 -28
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -69,6 +69,7 @@ export declare function assertCloudflareDurableObjectLifecycleSupported(options:
|
|
|
69
69
|
accountId: string;
|
|
70
70
|
apiBaseUrl: string;
|
|
71
71
|
readMigrationTag?: (options: CloudflareWorkerMigrationTagOptions) => Promise<string | null>;
|
|
72
|
+
allowPendingAtomicMigration?: boolean;
|
|
72
73
|
}): Promise<void>;
|
|
73
74
|
export declare function assertCloudflareBindingsPreserved(before: ExistingCloudflareWorker, after: ExistingCloudflareWorker, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>, options?: {
|
|
74
75
|
staticAssetsBinding?: string;
|
|
@@ -197,7 +198,7 @@ type CloudflareVersionActivation = {
|
|
|
197
198
|
trafficConfig?: CloudflareTrafficConfig;
|
|
198
199
|
} | {
|
|
199
200
|
ok: false;
|
|
200
|
-
reason: "uploader-changed" | "status-failed" | "deployment-changed" | "inheritance-source-unavailable" | "inheritance-source-changed" | "traffic-preservation-unavailable" | "traffic-split-unsupported" | "upload-failed" | "upload-output-invalid" | "preview-unavailable" | "readiness-staging-failed" | "readiness-failed" | "readiness-access-denied" | "bootstrap-failed" | "bootstrap-output-invalid" | "bootstrap-readiness-failed" | "bootstrap-readiness-access-denied" | "activation-failed" | "triggers-failed";
|
|
201
|
+
reason: "uploader-changed" | "status-failed" | "deployment-changed" | "inheritance-source-unavailable" | "inheritance-source-changed" | "traffic-preservation-unavailable" | "traffic-split-unsupported" | "upload-failed" | "upload-output-invalid" | "preview-unavailable" | "readiness-staging-failed" | "readiness-failed" | "readiness-access-denied" | "bootstrap-failed" | "bootstrap-output-invalid" | "bootstrap-readiness-failed" | "bootstrap-readiness-access-denied" | "atomic-failed" | "atomic-output-invalid" | "atomic-readiness-failed" | "atomic-readiness-access-denied" | "activation-failed" | "triggers-failed";
|
|
201
202
|
status?: number;
|
|
202
203
|
cloudflareErrorCode?: number;
|
|
203
204
|
/** Bounded uploader error text with terminal escapes removed and common credential formats redacted. */
|
|
@@ -273,7 +274,7 @@ type CloudflareVersionActivationOptions = {
|
|
|
273
274
|
preserveTriggers?: boolean;
|
|
274
275
|
expectedPreviousVersionId?: string;
|
|
275
276
|
trustedInheritanceVersionIds?: ReadonlyArray<string>;
|
|
276
|
-
recordVerifiedCandidate?: (versionId: string) => void | Promise<void>;
|
|
277
|
+
recordVerifiedCandidate?: (versionId: string, parentVersionId: string | null) => void | Promise<void>;
|
|
277
278
|
validateUploadedVersion?: (versionId: string) => Promise<void>;
|
|
278
279
|
runWrangler: CloudflareCommandRunner;
|
|
279
280
|
verifyUploader: () => boolean;
|
|
@@ -309,6 +310,8 @@ type CloudflareVersionActivationOptions = {
|
|
|
309
310
|
readinessAttempts?: number;
|
|
310
311
|
/** Verify that omitted remote secrets come from the active Worker Version before uploading. */
|
|
311
312
|
verifyInheritedSecrets?: boolean;
|
|
313
|
+
/** Explicitly publish an existing Durable Object Worker in one atomic operation. */
|
|
314
|
+
atomic?: boolean;
|
|
312
315
|
/**
|
|
313
316
|
* Plaintext secrets to attach only to the uploaded candidate version. Supplying every remote
|
|
314
317
|
* `secret_text` binding lets a deployment recover safely when an inactive upload is newer than
|
package/dist/index.mjs
CHANGED
|
@@ -246,13 +246,15 @@ async function assertCloudflareDurableObjectLifecycleSupported(options) {
|
|
|
246
246
|
const afterTag = await readMigrationTag(migrationOptions);
|
|
247
247
|
if (!after || after.snapshot.versionId !== existing.snapshot.versionId || afterTag !== activeTag) throw new Error("Cloudflare production or Durable Object migration state changed during verification. Retry from the current deployment.");
|
|
248
248
|
if (activeTag === null && existing.isSecretOnlyDraft && after.isSecretOnlyDraft) return;
|
|
249
|
+
if (activeTag === null && declarativeClassesMatch(migrations, existing.snapshot.runtime.exports)) return;
|
|
249
250
|
const activeIndex = activeTag === null ? -1 : migrations.findIndex((migration) => migration.tag === activeTag);
|
|
250
251
|
const pending = migrations.slice(activeIndex + 1).filter((migration) => migration.changesClasses);
|
|
251
252
|
if (pending.length === 0) return;
|
|
253
|
+
if (options.allowPendingAtomicMigration === true) return;
|
|
252
254
|
throw new Error([
|
|
253
|
-
"deploy:
|
|
255
|
+
"deploy: this Durable Object class migration needs an atomic Cloudflare deployment.",
|
|
254
256
|
`Pending migration tag${pending.length === 1 ? "" : "s"}: ${pending.map((migration) => migration.tag).join(", ")}.`,
|
|
255
|
-
"
|
|
257
|
+
"Review the migration, then run `void deploy --platform cloudflare --atomic`. The Worker will receive traffic before readiness is checked.",
|
|
256
258
|
"No database migration or Worker secret was changed."
|
|
257
259
|
].join("\n"));
|
|
258
260
|
}
|
|
@@ -270,10 +272,39 @@ function parseDurableObjectMigrations(value) {
|
|
|
270
272
|
for (const field of fields) if (field !== void 0 && !Array.isArray(field)) throw new Error("deploy: generated Cloudflare config has a malformed class migration.");
|
|
271
273
|
return {
|
|
272
274
|
tag: migration.tag,
|
|
273
|
-
changesClasses: fields.some((field) => Array.isArray(field) && field.length > 0)
|
|
275
|
+
changesClasses: fields.some((field) => Array.isArray(field) && field.length > 0),
|
|
276
|
+
step: migration
|
|
274
277
|
};
|
|
275
278
|
});
|
|
276
279
|
}
|
|
280
|
+
/** Cloudflare's declarative exports replace the legacy migration tag after an atomic publish. */
|
|
281
|
+
function declarativeClassesMatch(migrations, actualValue) {
|
|
282
|
+
if (!record$1(actualValue)) return false;
|
|
283
|
+
const desired = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const { step } of migrations) {
|
|
285
|
+
for (const [field, storage] of [["new_classes", "legacy-kv"], ["new_sqlite_classes", "sqlite"]]) for (const name of step[field] ?? []) {
|
|
286
|
+
if (typeof name !== "string") return false;
|
|
287
|
+
desired.set(name, storage);
|
|
288
|
+
}
|
|
289
|
+
for (const item of step.renamed_classes ?? []) {
|
|
290
|
+
if (!record$1(item) || typeof item.from !== "string" || typeof item.to !== "string") return false;
|
|
291
|
+
const storage = desired.get(item.from);
|
|
292
|
+
if (storage !== "sqlite" && storage !== "legacy-kv") return false;
|
|
293
|
+
desired.set(item.from, "deleted");
|
|
294
|
+
desired.set(item.to, storage);
|
|
295
|
+
}
|
|
296
|
+
for (const name of step.deleted_classes ?? []) {
|
|
297
|
+
if (typeof name !== "string") return false;
|
|
298
|
+
desired.set(name, "deleted");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (desired.size === 0) return false;
|
|
302
|
+
return [...desired].every(([name, storage]) => {
|
|
303
|
+
const current = actualValue[name];
|
|
304
|
+
if (storage === "deleted") return current === void 0 || record$1(current) && current.state === "deleted";
|
|
305
|
+
return record$1(current) && current.type === "durable-object" && current.storage === storage;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
277
308
|
function isMissingWorkerOutput$1(output) {
|
|
278
309
|
return /\[code:\s*10007\]/i.test(output) || /no deployments|\bworker\b.*\b(?:does not exist|not found)\b/i.test(output);
|
|
279
310
|
}
|
|
@@ -1151,7 +1182,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1151
1182
|
reason: "inheritance-source-unavailable"
|
|
1152
1183
|
};
|
|
1153
1184
|
const latestVersionId = recent.at(-1)?.id ?? null;
|
|
1154
|
-
if (options.expectedPreviousVersionId !== void 0) {
|
|
1185
|
+
if (options.expectedPreviousVersionId !== void 0 || options.trustedInheritanceVersionIds !== void 0) {
|
|
1155
1186
|
const activeIndex = recent.findIndex((version) => version.id === previous.versionId);
|
|
1156
1187
|
const inheritedChain = activeIndex < 0 ? [] : recent.slice(activeIndex);
|
|
1157
1188
|
const inheritedCandidates = inheritedChain.slice(1).map((version) => version.id);
|
|
@@ -1162,7 +1193,8 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1162
1193
|
rollback: "not-needed"
|
|
1163
1194
|
};
|
|
1164
1195
|
}
|
|
1165
|
-
|
|
1196
|
+
const trustedCandidateChain = options.trustedInheritanceVersionIds !== void 0 && options.trustedInheritanceVersionIds.length > 0 && previous.versionId !== latestVersionId;
|
|
1197
|
+
if (options.verifyInheritedSecrets === true && previous.versionId !== latestVersionId && !trustedCandidateChain) {
|
|
1166
1198
|
const replacesAllRemoteSecrets = await candidateReplacesAllRemoteSecrets(options, commonArgs, previous.versionId, latestVersionId);
|
|
1167
1199
|
if (replacesAllRemoteSecrets === null) return {
|
|
1168
1200
|
ok: false,
|
|
@@ -1214,6 +1246,10 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1214
1246
|
} : versionedPreviousTriggerConfig;
|
|
1215
1247
|
const previousTriggerConfig = triggerConfigWithQueueConsumerCleanup(versionedPreviousTriggerConfigWithTraffic, options.queueConsumerCleanup);
|
|
1216
1248
|
const alias = `void-ready-${Date.now().toString(36)}`;
|
|
1249
|
+
if (options.atomic && previous.versionId !== null) return bootstrapFirstDeployment({
|
|
1250
|
+
...options,
|
|
1251
|
+
message
|
|
1252
|
+
}, commonArgs, previous.versionId, true);
|
|
1217
1253
|
options.onProgress?.("Uploading the Worker and assets...");
|
|
1218
1254
|
const upload = await Promise.resolve(options.runWrangler([
|
|
1219
1255
|
"versions",
|
|
@@ -1278,8 +1314,8 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1278
1314
|
}
|
|
1279
1315
|
await options.validateUploadedVersion?.(versionId);
|
|
1280
1316
|
if (options.recordVerifiedCandidate) {
|
|
1281
|
-
if (!options.validateUploadedVersion) throw new Error("A Worker candidate cannot be recorded without validating its upload.");
|
|
1282
|
-
await options.recordVerifiedCandidate(versionId);
|
|
1317
|
+
if (!options.validateUploadedVersion && !inheritanceBaseline) throw new Error("A Worker candidate cannot be recorded without validating its upload and version sequence.");
|
|
1318
|
+
await options.recordVerifiedCandidate(versionId, previous.versionId);
|
|
1283
1319
|
}
|
|
1284
1320
|
const reportedPreviewUrl = output.match(PREVIEW_URL_RE)?.[1];
|
|
1285
1321
|
let authenticatedWorkersDevSubdomain = reportedPreviewUrl ? extractWorkersDevSubdomain(reportedPreviewUrl) : void 0;
|
|
@@ -1311,14 +1347,19 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1311
1347
|
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
|
|
1312
1348
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1313
1349
|
const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1314
|
-
if (!isCleanCommandExit(staged))
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1350
|
+
if (!isCleanCommandExit(staged)) {
|
|
1351
|
+
const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
|
|
1352
|
+
return {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
reason: "readiness-staging-failed",
|
|
1355
|
+
status: staged.status,
|
|
1356
|
+
...commandDiagnosticFields(staged),
|
|
1357
|
+
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
1358
|
+
versionId,
|
|
1359
|
+
previousVersionId: previous.versionId,
|
|
1360
|
+
rollback: "not-needed"
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1322
1363
|
stagedVersionOverride = true;
|
|
1323
1364
|
}
|
|
1324
1365
|
options.onProgress?.("Waiting for the Worker to become ready...");
|
|
@@ -1336,14 +1377,19 @@ async function activateCloudflareVersionWithSecretsFile(options) {
|
|
|
1336
1377
|
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
|
|
1337
1378
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1338
1379
|
const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1339
|
-
if (!isCleanCommandExit(staged))
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1380
|
+
if (!isCleanCommandExit(staged)) {
|
|
1381
|
+
const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
|
|
1382
|
+
return {
|
|
1383
|
+
ok: false,
|
|
1384
|
+
reason: "readiness-staging-failed",
|
|
1385
|
+
status: staged.status,
|
|
1386
|
+
...commandDiagnosticFields(staged),
|
|
1387
|
+
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
1388
|
+
versionId,
|
|
1389
|
+
previousVersionId: previous.versionId,
|
|
1390
|
+
rollback: "not-needed"
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1347
1393
|
readinessUrl = stableReadinessUrl;
|
|
1348
1394
|
authenticatedWorkersDevSubdomain = extractWorkersDevSubdomain(stableReadinessUrl);
|
|
1349
1395
|
readinessUsesCustomRoute = !desiredTriggerConfig.workers_dev;
|
|
@@ -1680,12 +1726,26 @@ async function readCloudflareVersionSecretBindings(options, commonArgs, versionI
|
|
|
1680
1726
|
* brand-new Worker can therefore need one ordinary deploy before later releases use preview-first
|
|
1681
1727
|
* version activation. This fallback requires prior proof that no application had been deployed.
|
|
1682
1728
|
*/
|
|
1683
|
-
async function bootstrapFirstDeployment(options, commonArgs, previousVersionId) {
|
|
1729
|
+
async function bootstrapFirstDeployment(options, commonArgs, previousVersionId, atomic = false) {
|
|
1684
1730
|
if (!options.verifyUploader()) return {
|
|
1685
1731
|
ok: false,
|
|
1686
1732
|
reason: "uploader-changed"
|
|
1687
1733
|
};
|
|
1688
|
-
|
|
1734
|
+
if (atomic) {
|
|
1735
|
+
const current = await readCurrentProductionVersion(options, commonArgs);
|
|
1736
|
+
if (current.kind === "failed") return {
|
|
1737
|
+
ok: false,
|
|
1738
|
+
reason: "status-failed",
|
|
1739
|
+
status: current.status
|
|
1740
|
+
};
|
|
1741
|
+
if (current.kind !== "ok" || current.versionId !== previousVersionId) return {
|
|
1742
|
+
ok: false,
|
|
1743
|
+
reason: "deployment-changed",
|
|
1744
|
+
activeVersionId: current.kind === "ok" ? current.versionId : null,
|
|
1745
|
+
rollback: "not-needed"
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
options.onProgress?.(atomic ? "Publishing the Worker atomically..." : "Publishing the first Worker deployment...");
|
|
1689
1749
|
const bootstrap = await Promise.resolve(options.runWrangler([
|
|
1690
1750
|
"deploy",
|
|
1691
1751
|
...commonArgs,
|
|
@@ -1705,7 +1765,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1705
1765
|
const cloudflareErrorCode = parseCloudflareErrorCode(output);
|
|
1706
1766
|
return {
|
|
1707
1767
|
ok: false,
|
|
1708
|
-
reason: "bootstrap-failed",
|
|
1768
|
+
reason: atomic ? "atomic-failed" : "bootstrap-failed",
|
|
1709
1769
|
status: bootstrap.status,
|
|
1710
1770
|
...commandDiagnosticFields(bootstrap),
|
|
1711
1771
|
...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
|
|
@@ -1719,7 +1779,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1719
1779
|
const reportedOrConfiguredUrl = reportedUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
|
|
1720
1780
|
if (!versionId || !reportedOrConfiguredUrl) return {
|
|
1721
1781
|
ok: false,
|
|
1722
|
-
reason: "bootstrap-output-invalid",
|
|
1782
|
+
reason: atomic ? "atomic-output-invalid" : "bootstrap-output-invalid",
|
|
1723
1783
|
previousVersionId,
|
|
1724
1784
|
...versionId ? { versionId } : {}
|
|
1725
1785
|
};
|
|
@@ -1728,7 +1788,7 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1728
1788
|
const ready = await probeReadiness(`${previewUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, reportedUrl || desiredTriggerConfig.workers_dev ? options.readinessHeaders : void 0, extractWorkersDevSubdomain(previewUrl), void 0, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 30, 6e4);
|
|
1729
1789
|
if (ready !== "ready") return {
|
|
1730
1790
|
ok: false,
|
|
1731
|
-
reason: ready === "access-denied" ? "bootstrap-readiness-access-denied" : "bootstrap-readiness-failed",
|
|
1791
|
+
reason: ready === "access-denied" ? atomic ? "atomic-readiness-access-denied" : "bootstrap-readiness-access-denied" : atomic ? "atomic-readiness-failed" : "bootstrap-readiness-failed",
|
|
1732
1792
|
versionId,
|
|
1733
1793
|
previousVersionId,
|
|
1734
1794
|
rollback: "unavailable"
|
|
@@ -1738,6 +1798,11 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId)
|
|
|
1738
1798
|
versionId,
|
|
1739
1799
|
previewUrl,
|
|
1740
1800
|
previousVersionId,
|
|
1801
|
+
...atomic ? { trafficConfig: {
|
|
1802
|
+
workers_dev: desiredTriggerConfig.workers_dev,
|
|
1803
|
+
preview_urls: desiredTriggerConfig.preview_urls,
|
|
1804
|
+
routes: desiredTriggerConfig.routes
|
|
1805
|
+
} } : {},
|
|
1741
1806
|
...workersDevSubdomainResult(previewUrl)
|
|
1742
1807
|
};
|
|
1743
1808
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/deploy-cloudflare",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@void/deploy-core": "0.21.
|
|
23
|
+
"@void/deploy-core": "0.21.3",
|
|
24
24
|
"jsonc-parser": "3.3.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|