@void/deploy-cloudflare 0.21.9 → 0.23.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/dist/index.d.mts CHANGED
@@ -1,13 +1,120 @@
1
1
  import { AssetConfig, HeaderRule, RedirectRule } from "@void/deploy-core";
2
+ //#region src/operations.d.ts
3
+ /** Typed provider operations shared by app deployment and platform installation. */
4
+ type ConfigTarget = {
5
+ configPath?: string;
6
+ };
7
+ type WorkerTarget = ConfigTarget & {
8
+ workerName?: string;
9
+ };
10
+ type CloudflareOperation = ({
11
+ type: "auth.whoami" | "auth.login" | "auth.logout" | "auth.token";
12
+ } & ConfigTarget) | ({
13
+ type: "d1.list" | "kv.namespace.list" | "containers.list";
14
+ } & ConfigTarget) | ({
15
+ type: "d1.info";
16
+ name: string;
17
+ } & ConfigTarget) | {
18
+ type: "d1.create";
19
+ name: string;
20
+ binding: string;
21
+ configPath: string;
22
+ } | {
23
+ type: "d1.execute";
24
+ database: string;
25
+ sql: string;
26
+ configPath: string;
27
+ } | {
28
+ type: "d1.migrations.list" | "d1.migrations.apply";
29
+ database: string;
30
+ configPath: string;
31
+ } | {
32
+ type: "kv.namespace.create";
33
+ name: string;
34
+ binding: string;
35
+ configPath: string;
36
+ } | ({
37
+ type: "r2.bucket.info";
38
+ name: string;
39
+ } & ConfigTarget) | {
40
+ type: "r2.bucket.create";
41
+ name: string;
42
+ binding: string;
43
+ configPath: string;
44
+ } | {
45
+ type: "hyperdrive.create";
46
+ name: string;
47
+ binding: string;
48
+ configPath: string;
49
+ } | ({
50
+ type: "queues.info" | "queues.create";
51
+ name: string;
52
+ } & ConfigTarget) | ({
53
+ type: "queues.consumer.remove";
54
+ queueName: string;
55
+ workerName: string;
56
+ } & ConfigTarget) | ({
57
+ type: "email.routing.settings" | "email.routing.enable" | "email.routing.rules.list" | "email.sending.settings" | "email.sending.enable";
58
+ domain: string;
59
+ } & ConfigTarget) | ({
60
+ type: "secret.list" | "secret.bulk";
61
+ } & WorkerTarget) | ({
62
+ type: "secret.put" | "secret.delete";
63
+ name: string;
64
+ } & WorkerTarget) | ({
65
+ type: "deployments.status" | "deployments.list" | "versions.list";
66
+ } & WorkerTarget) | ({
67
+ type: "versions.view";
68
+ versionId: string;
69
+ } & WorkerTarget) | ({
70
+ type: "versions.deploy";
71
+ versions: Array<{
72
+ versionId: string;
73
+ percentage: number;
74
+ }>;
75
+ message?: string;
76
+ } & WorkerTarget) | ({
77
+ type: "versions.upload" | "worker.deploy";
78
+ message?: string;
79
+ secretsFilePath?: string;
80
+ assetsDirectory?: string;
81
+ previewAlias?: string;
82
+ noBundle?: boolean;
83
+ } & WorkerTarget) | ({
84
+ type: "triggers.deploy";
85
+ assets?: boolean;
86
+ } & WorkerTarget) | ({
87
+ type: "triggers.clear-crons" | "triggers.clear-domains";
88
+ } & WorkerTarget) | ({
89
+ type: "tail";
90
+ search?: string;
91
+ versionId?: string;
92
+ status?: "error";
93
+ } & WorkerTarget);
94
+ type CloudflareUploadResult = {
95
+ versionId: string;
96
+ previewUrl?: string;
97
+ workersDevUrl?: string;
98
+ };
99
+ type CloudflareProviderError = {
100
+ code?: number;
101
+ message: string;
102
+ };
103
+ //#endregion
2
104
  //#region src/execute.d.ts
3
105
  type CloudflareCommandResult = {
106
+ /** Structured provider response. stdout is diagnostics only. */
107
+ data?: unknown;
108
+ errorCode?: number;
109
+ failure?: "not-found" | "durable-migration" | "containers-unavailable";
110
+ errors?: Array<CloudflareProviderError>;
4
111
  stdout: string;
5
112
  stderr: string;
6
113
  status: number;
7
114
  signal?: NodeJS.Signals | null;
8
115
  error?: string | null;
9
116
  };
10
- type CloudflareCommandRunner = (args: Array<string>, options: {
117
+ type CloudflareCommandRunner = (operation: CloudflareOperation, options: {
11
118
  capture: boolean;
12
119
  }) => CloudflareCommandResult | Promise<CloudflareCommandResult>;
13
120
  type CloudflareDeployExecution = {
@@ -112,6 +219,7 @@ type CloudflareFrameworkDeployConfig = Readonly<{
112
219
  assetsIgnore?: ReadonlyArray<string>;
113
220
  crons?: ReadonlyArray<string>;
114
221
  requiredSecrets?: ReadonlyArray<string>;
222
+ versionMetadataBinding?: string;
115
223
  ai?: Readonly<{
116
224
  binding: string;
117
225
  gateway?: string;
@@ -142,6 +250,7 @@ export declare function renderCloudflareStaticEntry(options: {
142
250
  headerRules?: ReadonlyArray<HeaderRule>;
143
251
  redirectRules?: ReadonlyArray<RedirectRule>;
144
252
  fallbackRules?: ReadonlyArray<RedirectRule>;
253
+ versionMetadataBinding?: string;
145
254
  }): string;
146
255
  /**
147
256
  * Copy static assets into Void's deploy area without Cloudflare's reserved routing files.
@@ -314,6 +423,10 @@ type CloudflareVersionActivationOptions = {
314
423
  fetch?: typeof globalThis.fetch;
315
424
  sleep?: (milliseconds: number) => Promise<void>;
316
425
  readinessAttempts?: number;
426
+ /** Require readiness to report the uploaded Worker's actual version UUID. */
427
+ requireVersionMetadata?: boolean;
428
+ /** A binding-preserving handoff without metadata may use only a genuine version preview. */
429
+ requirePreviewForReadiness?: boolean;
317
430
  /** Verify that omitted remote secrets come from the active Worker Version before uploading. */
318
431
  verifyInheritedSecrets?: boolean;
319
432
  /** Pin opaque secret bindings to the live version when an unpublished upload is newer. */
@@ -364,4 +477,4 @@ export declare function createCloudflareMetadataRunner(options: {
364
477
  env?: Readonly<Record<string, string | undefined>>;
365
478
  }): CloudflareCommandRunner;
366
479
  //#endregion
367
- export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
480
+ export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareOperation, CloudflareProviderError, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareUploadResult, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
package/dist/index.mjs CHANGED
@@ -115,25 +115,18 @@ async function readExistingCloudflareWorker(options) {
115
115
  return worker.snapshot;
116
116
  }
117
117
  async function readCloudflareWorker(options, allowMissing) {
118
- const target = [
119
- "-c",
120
- options.configPath,
121
- "--env=",
122
- "--name",
123
- options.workerName
124
- ];
125
- const read = async (args, missingIsNull = false) => {
126
- const result = await options.runWrangler([
127
- ...args,
128
- ...target,
129
- "--json"
130
- ], { capture: true });
118
+ const target = {
119
+ configPath: options.configPath,
120
+ workerName: options.workerName
121
+ };
122
+ const read = async (operation, missingIsNull = false) => {
123
+ const result = await options.runWrangler(operation, { capture: true });
131
124
  if (result.status !== 0 || result.signal || result.error) {
132
- if (missingIsNull && isMissingWorkerOutput$1(`${result.stdout}\n${result.stderr}`)) return null;
125
+ if (missingIsNull && Number.isSafeInteger(result.status) && result.status >= 0 && result.signal == null && (result.error == null || result.error === "") && (result.failure === "not-found" || result.errorCode === 10007)) return null;
133
126
  throw new CloudflareWorkerInspectionError(`Could not verify existing Cloudflare Worker "${options.workerName}". Check the account, Worker name, and Cloudflare permissions before retrying.`);
134
127
  }
135
128
  try {
136
- const value = JSON.parse(result.stdout);
129
+ const value = result.data;
137
130
  if (record$1(value)) return value;
138
131
  } catch {}
139
132
  throw new Error("Cloudflare returned an invalid existing Worker response.");
@@ -141,21 +134,27 @@ async function readCloudflareWorker(options, allowMissing) {
141
134
  const deployment = options.versionId ? { versions: [{
142
135
  version_id: options.versionId,
143
136
  percentage: 100
144
- }] } : await read(["deployments", "status"], allowMissing);
137
+ }] } : await read({
138
+ type: "deployments.status",
139
+ ...target
140
+ }, allowMissing);
145
141
  if (deployment === null) return null;
146
142
  const versions = deployment.versions;
147
143
  if (allowMissing && Array.isArray(versions) && versions.length === 0) return null;
148
144
  if (!Array.isArray(versions) || versions.length !== 1 || !record$1(versions[0]) || versions[0].percentage !== 100 || typeof versions[0].version_id !== "string") throw new Error("The existing Worker must have one deployed version receiving all traffic. Finish any gradual rollout before linking.");
149
145
  const versionId = versions[0].version_id;
150
- const version = await read([
151
- "versions",
152
- "view",
153
- versionId
154
- ]);
146
+ const version = await read({
147
+ type: "versions.view",
148
+ versionId,
149
+ ...target
150
+ });
155
151
  if (version === null) throw new Error("Cloudflare did not return the existing Worker configuration.");
156
152
  const resources = version.resources;
157
153
  if (version.id !== versionId || !record$1(resources) || !Array.isArray(resources.bindings) || !resources.bindings.every(record$1) || !record$1(resources.script_runtime) || !record$1(resources.script)) throw new Error("Cloudflare did not return the existing Worker configuration.");
158
- const after = options.versionId ? deployment : await read(["deployments", "status"]);
154
+ const after = options.versionId ? deployment : await read({
155
+ type: "deployments.status",
156
+ ...target
157
+ });
159
158
  if (after === null || !isDeepStrictEqual(deployment.versions, after.versions)) throw new Error("Cloudflare production changed while linking. Retry from the current deployment.");
160
159
  return {
161
160
  snapshot: {
@@ -207,15 +206,11 @@ async function readCloudflareWorkerMigrationTag(options) {
207
206
  return worker.migration_tag;
208
207
  }
209
208
  async function readCloudflareApiAuthentication(runWrangler) {
210
- const result = await runWrangler([
211
- "auth",
212
- "token",
213
- "--json"
214
- ], { capture: true });
209
+ const result = await runWrangler({ type: "auth.token" }, { capture: true });
215
210
  if (result.status !== 0 || result.signal || result.error) throw new Error("Could not read the authenticated Cloudflare session.");
216
211
  let value;
217
212
  try {
218
- value = JSON.parse(result.stdout);
213
+ value = result.data;
219
214
  } catch {
220
215
  throw new Error("Cloudflare returned an invalid authentication response.");
221
216
  }
@@ -309,9 +304,6 @@ function declarativeClassesMatch(migrations, actualValue) {
309
304
  return record$1(current) && current.type === "durable-object" && current.storage === storage;
310
305
  });
311
306
  }
312
- function isMissingWorkerOutput$1(output) {
313
- return /\[code:\s*10007\]/i.test(output) || /no deployments|\bworker\b.*\b(?:does not exist|not found)\b/i.test(output);
314
- }
315
307
  function assertCloudflareBindingsPreserved(before, after, approvedKv = [], options = {}) {
316
308
  const staticAssetsBinding = isAssetsOnlyWorker(before) ? options.staticAssetsBinding : void 0;
317
309
  const addsAssetsBinding = staticAssetsBinding !== void 0 && !before.bindings.some((binding) => binding.name === staticAssetsBinding);
@@ -381,6 +373,9 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
381
373
  if ((built || config[field] !== void 0) && !equal) throw new Error(`Keep the existing Worker's ${field} when linking. Update it in a later deployment.`);
382
374
  }
383
375
  const vars = record$1(config.vars) ? config.vars : {};
376
+ const metadataBindings = existing.bindings.filter((binding) => binding.type === "version_metadata");
377
+ const metadata = record$1(config.version_metadata) ? config.version_metadata : void 0;
378
+ if (metadataBindings.length > 1 || metadataBindings.length === 1 && metadata?.binding !== metadataBindings[0].name || metadataBindings.length === 0 && config.version_metadata !== void 0) throw new Error("Keep the existing Worker version_metadata binding when linking; linking cannot add or replace it.");
384
379
  const remote = new Map(existing.bindings.map((binding) => [binding.name, binding]));
385
380
  for (const [name, value] of Object.entries(vars)) {
386
381
  const binding = remote.get(name);
@@ -423,6 +418,7 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
423
418
  "secret_text",
424
419
  "secret_key"
425
420
  ].includes(String(binding.type))) continue;
421
+ if (binding.type === "version_metadata") continue;
426
422
  if (binding.type === "assets") {
427
423
  if (built && (!record$1(config.assets) || config.assets.binding !== binding.name)) throw new Error(`Keep the existing assets binding ${binding.name} when linking.`);
428
424
  continue;
@@ -478,14 +474,10 @@ async function executeCloudflareDeploy(options) {
478
474
  ok: false,
479
475
  reason: "uploader-changed"
480
476
  };
481
- const result = await Promise.resolve(options.runWrangler([
482
- "deploy",
483
- ...options.configPath ? ["-c", options.configPath] : [],
484
- "--env=",
485
- "--experimental-provision=false",
486
- "--experimental-auto-create=false",
487
- "--autoconfig=false"
488
- ], { capture: false }));
477
+ const result = await Promise.resolve(options.runWrangler({
478
+ type: "worker.deploy",
479
+ ...options.configPath ? { configPath: options.configPath } : {}
480
+ }, { capture: false }));
489
481
  if (!isCleanCommandExit$1(result)) return {
490
482
  ok: false,
491
483
  reason: "deploy-failed",
@@ -558,6 +550,7 @@ function writeCloudflareFrameworkDeployConfig(options) {
558
550
  upsert(["account_id"], options.accountId);
559
551
  upsert(["main"], options.main);
560
552
  upsert(["compatibility_date"], options.compatibilityDate);
553
+ if (options.versionMetadataBinding && parse(content).version_metadata === void 0) upsert(["version_metadata"], { binding: options.versionMetadataBinding });
561
554
  if (options.compatibilityFlags && options.compatibilityFlags.length > 0) {
562
555
  const config = parse(content);
563
556
  const current = Array.isArray(config.compatibility_flags) ? config.compatibility_flags.filter((flag) => typeof flag === "string") : [];
@@ -615,7 +608,8 @@ function renderCloudflareStaticEntry(options) {
615
608
  const config = JSON.stringify({
616
609
  ...options.headerRules?.length ? { headerRules: options.headerRules } : {},
617
610
  ...options.redirectRules?.length ? { redirectRules: options.redirectRules } : {},
618
- ...options.fallbackRules?.length ? { fallbackRules: options.fallbackRules } : {}
611
+ ...options.fallbackRules?.length ? { fallbackRules: options.fallbackRules } : {},
612
+ ...options.versionMetadataBinding ? { versionMetadataBinding: options.versionMetadataBinding } : {}
619
613
  });
620
614
  return [
621
615
  `import { createStaticEdgeHandler } from ${JSON.stringify(options.runtimeImport ?? "@void/edge")};`,
@@ -762,10 +756,6 @@ function cloudflareCommandDiagnostic(result) {
762
756
  }
763
757
  //#endregion
764
758
  //#region src/version.ts
765
- const VERSION_ID_RE = /(?:Worker|Current) Version ID:\s*([0-9a-f]{8}-[0-9a-f-]{27})/i;
766
- const PREVIEW_URL_RE = /Version Preview(?: Alias)? URL:\s*(https:\/\/\S+)/i;
767
- const WORKERS_DEV_URL_RE = /(https:\/\/[a-z0-9.-]+\.workers\.dev(?:\/[^\s]*)?)/i;
768
- const CLOUDFLARE_ERROR_CODE_RE = /\[code:\s*(\d+)\]/i;
769
759
  const DURABLE_MIGRATION_VERSION_ERROR = 10211;
770
760
  /** Encode migration state into Cloudflare's version annotation for safe rollback diagnostics. */
771
761
  function encodeCloudflareVersionMessage(migrations, triggerConfig, contentHash) {
@@ -830,31 +820,23 @@ async function deployCloudflareVersionTriggers(options) {
830
820
  const configPath = join(directory, "wrangler.json");
831
821
  try {
832
822
  writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
833
- const deployed = await options.runWrangler([
834
- "triggers",
835
- "deploy",
836
- "-c",
823
+ const deployed = await options.runWrangler({
824
+ type: "triggers.deploy",
837
825
  configPath,
838
- "--env=",
839
- "--name",
840
- options.workerName
841
- ], { capture: false });
826
+ workerName: options.workerName
827
+ }, { capture: false });
842
828
  if (!isCleanCommandExit(deployed)) return deployed;
843
829
  if (options.triggerConfig.crons.length === 0 && options.publishEmptyCrons) {
844
830
  const published = await options.publishEmptyCrons();
845
831
  if (!isCleanCommandExit(published)) return published;
846
832
  }
847
833
  for (const queue of removedQueueConsumers(options.triggerConfig, options.previousTriggerConfig)) {
848
- const removed = await options.runWrangler([
849
- "queues",
850
- "consumer",
851
- "remove",
852
- queue,
853
- options.workerName,
854
- "-c",
855
- configPath,
856
- "--env="
857
- ], { capture: false });
834
+ const removed = await options.runWrangler({
835
+ type: "queues.consumer.remove",
836
+ queueName: queue,
837
+ workerName: options.workerName,
838
+ configPath
839
+ }, { capture: false });
858
840
  if (!isCleanCommandExit(removed)) return removed;
859
841
  }
860
842
  if (options.manageRoutes !== false && !options.triggerConfig.preserveRoutes && hasCustomDomains(options.previousTriggerConfig?.routes) && !hasCustomDomains(options.triggerConfig.routes)) {
@@ -894,11 +876,11 @@ function pickWranglerTriggerContext(source) {
894
876
  * schema-2 snapshot. The bounded loop fails closed if traffic never stabilizes.
895
877
  */
896
878
  async function stabilizeCloudflareVersionTriggers(options) {
897
- const commonArgs = [...options.configPath ? ["-c", options.configPath] : [], "--env="];
879
+ const configPath = options.configPath;
898
880
  let cleanupBaseline = mergeTriggerCleanupBaselines(options.previousTriggerConfig, options.targetTriggerConfig);
899
881
  const attempts = Math.max(1, options.attempts ?? 4);
900
882
  for (let attempt = 0; attempt < attempts; attempt++) {
901
- const owner = await readCurrentProductionVersion(options, commonArgs);
883
+ const owner = await readCurrentProductionVersion(options, configPath);
902
884
  if (owner.kind === "failed") return {
903
885
  ok: false,
904
886
  reason: "status-failed",
@@ -916,7 +898,7 @@ async function stabilizeCloudflareVersionTriggers(options) {
916
898
  const activeVersionId = owner.versionId;
917
899
  let activeTriggerConfig = activeVersionId === options.targetVersionId ? options.targetTriggerConfig : options.knownTriggerConfigs?.[activeVersionId];
918
900
  if (!activeTriggerConfig && activeVersionId !== options.targetVersionId) {
919
- const metadata = await readCloudflareVersionMetadata(options, commonArgs, activeVersionId);
901
+ const metadata = await readCloudflareVersionMetadata(options, configPath, activeVersionId);
920
902
  activeTriggerConfig = metadata?.schema === 2 ? metadata.triggerConfig : void 0;
921
903
  }
922
904
  if (!activeTriggerConfig) return {
@@ -941,7 +923,7 @@ async function stabilizeCloudflareVersionTriggers(options) {
941
923
  activeVersionId
942
924
  };
943
925
  cleanupBaseline = mergeTriggerCleanupBaselines(cleanupBaseline, activeTriggerConfig);
944
- const after = await readCurrentProductionVersion(options, commonArgs);
926
+ const after = await readCurrentProductionVersion(options, configPath);
945
927
  if (after.kind === "failed") return {
946
928
  ok: false,
947
929
  reason: "status-failed",
@@ -1139,6 +1121,15 @@ function resolvePreservedTraffic(desired, previous, workerExists) {
1139
1121
  function isRecord(value) {
1140
1122
  return typeof value === "object" && value !== null && !Array.isArray(value);
1141
1123
  }
1124
+ function isCandidatePreviewUrl(value, workerName, versionId, alias) {
1125
+ try {
1126
+ const url = new URL(value);
1127
+ const label = url.hostname.split(".")[0];
1128
+ return url.protocol === "https:" && !url.username && !url.password && extractWorkersDevSubdomain(value) !== void 0 && (label === `${versionId.slice(0, 8)}-${workerName}` || label === `${alias}-${workerName}`);
1129
+ } catch {
1130
+ return false;
1131
+ }
1132
+ }
1142
1133
  /** Upload, probe, and atomically activate a verified Worker Version. */
1143
1134
  async function activateCloudflareVersion(options) {
1144
1135
  const candidateSecrets = Object.entries(options.candidateSecrets ?? {});
@@ -1159,13 +1150,13 @@ async function activateCloudflareVersion(options) {
1159
1150
  }
1160
1151
  }
1161
1152
  async function activateCloudflareVersionWithSecretsFile(options) {
1162
- const commonArgs = [...options.configPath ? ["-c", options.configPath] : [], "--env="];
1153
+ const configPath = options.configPath;
1163
1154
  if (!options.verifyUploader()) return {
1164
1155
  ok: false,
1165
1156
  reason: "uploader-changed"
1166
1157
  };
1167
1158
  options.onProgress?.("Checking the current Worker deployment...");
1168
- const previous = await readCurrentProductionVersion(options, commonArgs);
1159
+ const previous = await readCurrentProductionVersion(options, configPath);
1169
1160
  if (previous.kind === "failed") return {
1170
1161
  ok: false,
1171
1162
  reason: "status-failed",
@@ -1186,22 +1177,30 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1186
1177
  reason: "uploader-changed"
1187
1178
  };
1188
1179
  const previousVersionId = previous.versionId;
1180
+ if (options.atomic && options.requirePreviewForReadiness) return {
1181
+ ok: false,
1182
+ reason: "preview-unavailable",
1183
+ previousVersionId,
1184
+ rollback: "not-needed",
1185
+ diagnostic: "This binding-preserving handoff requires a candidate preview before activation. Configure version metadata on the existing Worker or retry without atomic publication; production traffic is unchanged."
1186
+ };
1187
+ const trustedInheritanceVersionIds = previousVersionId && options.trustedInheritanceVersionIds?.at(-1) === previousVersionId ? [] : options.trustedInheritanceVersionIds;
1189
1188
  let inheritanceBaseline;
1190
1189
  let trustedCandidateChain = false;
1191
1190
  let activeSecretsPinned = false;
1192
1191
  if (options.expectedPreviousVersionId !== void 0 || options.verifyInheritedSecrets === true) {
1193
- const recent = await readRecentCloudflareVersionIds(options, commonArgs);
1192
+ const recent = await readRecentCloudflareVersionIds(options, configPath);
1194
1193
  if (recent === null) return {
1195
1194
  ok: false,
1196
1195
  reason: "inheritance-source-unavailable"
1197
1196
  };
1198
1197
  const latestVersionId = recent.at(-1)?.id ?? null;
1199
- if (options.expectedPreviousVersionId !== void 0 || options.trustedInheritanceVersionIds !== void 0) {
1198
+ if (options.expectedPreviousVersionId !== void 0 || trustedInheritanceVersionIds !== void 0) {
1200
1199
  const activeIndex = recent.findIndex((version) => version.id === previous.versionId);
1201
1200
  const inheritedChain = activeIndex < 0 ? [] : recent.slice(activeIndex);
1202
1201
  const inheritedCandidates = inheritedChain.slice(1).map((version) => version.id);
1203
- trustedCandidateChain = options.trustedInheritanceVersionIds !== void 0 && options.trustedInheritanceVersionIds.length > 0 && arraysEqual(inheritedCandidates, options.trustedInheritanceVersionIds);
1204
- if (!(inheritedChain.length > 0 && inheritedChain.every((version, index) => index === 0 || version.number === inheritedChain[index - 1].number + 1) && (arraysEqual(inheritedCandidates, options.trustedInheritanceVersionIds ?? []) || options.pinActiveSecrets !== void 0))) return {
1202
+ trustedCandidateChain = trustedInheritanceVersionIds !== void 0 && trustedInheritanceVersionIds.length > 0 && arraysEqual(inheritedCandidates, trustedInheritanceVersionIds);
1203
+ if (!(inheritedChain.length > 0 && inheritedChain.every((version, index) => index === 0 || version.number === inheritedChain[index - 1].number + 1) && (arraysEqual(inheritedCandidates, trustedInheritanceVersionIds ?? []) || options.pinActiveSecrets !== void 0))) return {
1205
1204
  ok: false,
1206
1205
  reason: "inheritance-source-changed",
1207
1206
  activeVersionId: recent.at(-1)?.id ?? null,
@@ -1209,13 +1208,13 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1209
1208
  };
1210
1209
  }
1211
1210
  if (options.verifyInheritedSecrets === true && previous.versionId !== latestVersionId && !trustedCandidateChain) {
1212
- const replacesAllRemoteSecrets = await candidateReplacesAllRemoteSecrets(options, commonArgs, previous.versionId, latestVersionId);
1211
+ const replacesAllRemoteSecrets = await candidateReplacesAllRemoteSecrets(options, configPath, previous.versionId, latestVersionId);
1213
1212
  if (replacesAllRemoteSecrets === null) return {
1214
1213
  ok: false,
1215
1214
  reason: "inheritance-source-unavailable"
1216
1215
  };
1217
1216
  if (!replacesAllRemoteSecrets) {
1218
- const activeSecrets = previous.versionId ? await readCloudflareVersionSecretBindings(options, commonArgs, previous.versionId) : null;
1217
+ const activeSecrets = previous.versionId ? await readCloudflareVersionSecretBindings(options, configPath, previous.versionId) : null;
1219
1218
  if (!activeSecrets || !options.pinActiveSecrets || !await options.pinActiveSecrets(previous.versionId, activeSecrets)) return {
1220
1219
  ok: false,
1221
1220
  reason: "inheritance-source-changed",
@@ -1229,10 +1228,10 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1229
1228
  }
1230
1229
  const desiredMetadata = parseCloudflareVersionMessage(options.message);
1231
1230
  let desiredTriggerConfig = desiredMetadata?.schema === 2 ? desiredMetadata.triggerConfig : options.configPath ? readCloudflareTriggerConfig(options.configPath) : triggerConfigFromWrangler({});
1232
- let previousVersion = previousVersionId && desiredMetadata?.schema === 2 ? await readCloudflareVersionInspection(options, commonArgs, previousVersionId, inheritanceBaseline?.map((version) => version.inspection)) : null;
1231
+ let previousVersion = previousVersionId && desiredMetadata?.schema === 2 ? await readCloudflareVersionInspection(options, configPath, previousVersionId, inheritanceBaseline?.map((version) => version.inspection)) : null;
1233
1232
  async function previousIsSecretOnlyDraft() {
1234
1233
  if (!previousVersionId) return false;
1235
- previousVersion ??= await readCloudflareVersionInspection(options, commonArgs, previousVersionId, inheritanceBaseline?.map((version) => version.inspection));
1234
+ previousVersion ??= await readCloudflareVersionInspection(options, configPath, previousVersionId, inheritanceBaseline?.map((version) => version.inspection));
1236
1235
  return previousVersion?.isSecretOnlyDraft === true;
1237
1236
  }
1238
1237
  const previousMetadata = desiredMetadata?.schema === 2 ? previousVersion?.metadata : null;
@@ -1266,57 +1265,49 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1266
1265
  const alias = `void-ready-${Date.now().toString(36)}`;
1267
1266
  if (options.atomic && previous.versionId !== null) {
1268
1267
  if (!activeSecretsPinned && options.pinActiveSecrets) {
1269
- const activeSecrets = await readCloudflareVersionSecretBindings(options, commonArgs, previous.versionId);
1268
+ const activeSecrets = await readCloudflareVersionSecretBindings(options, configPath, previous.versionId);
1270
1269
  if (activeSecrets?.length) await options.pinActiveSecrets(previous.versionId, activeSecrets);
1271
1270
  }
1272
1271
  return bootstrapFirstDeployment({
1273
1272
  ...options,
1274
1273
  message
1275
- }, commonArgs, previous.versionId, true);
1274
+ }, configPath, previous.versionId, true, previousTriggerConfig);
1276
1275
  }
1277
1276
  options.onProgress?.("Uploading the Worker and assets...");
1278
- const upload = await Promise.resolve(options.runWrangler([
1279
- "versions",
1280
- "upload",
1281
- ...commonArgs,
1282
- "--name",
1283
- options.workerName,
1284
- "--message",
1277
+ const upload = await Promise.resolve(options.runWrangler({
1278
+ type: "versions.upload",
1279
+ configPath,
1280
+ workerName: options.workerName,
1285
1281
  message,
1286
- ...options.candidateSecretsFilePath ? ["--secrets-file", options.candidateSecretsFilePath] : [],
1287
- ...options.assetsDirectoryOverride ? ["--assets", options.assetsDirectoryOverride] : [],
1288
- "--preview-alias",
1289
- alias,
1290
- "--strict",
1291
- "--experimental-provision=false",
1292
- "--experimental-auto-create=false"
1293
- ], { capture: true }));
1282
+ secretsFilePath: options.candidateSecretsFilePath,
1283
+ assetsDirectory: options.assetsDirectoryOverride,
1284
+ previewAlias: alias
1285
+ }, { capture: true }));
1294
1286
  if (!isCleanCommandExit(upload)) {
1295
- const output = stripVTControlCharacters(`${upload.stdout}\n${upload.stderr}`);
1296
- const cloudflareErrorCode = parseCloudflareErrorCode(output);
1297
- const isBootstrapError = cloudflareErrorCode === DURABLE_MIGRATION_VERSION_ERROR || /This\s+Worker\s+has\s+a\s+pending\s+Durable\s+Object\s+migration,\s+which\s+cannot\s+be\s+applied\s+by\s+`wrangler\s+versions\s+upload`/i.test(output) || /cannot upload a new version of a worker that does not yet exist/i.test(output);
1287
+ const cloudflareErrorCode = upload.errorCode;
1288
+ const isBootstrapError = isCompletedCommand(upload) && (upload.failure === "durable-migration" || upload.failure === "not-found" || cloudflareErrorCode === DURABLE_MIGRATION_VERSION_ERROR || cloudflareErrorCode === 10007);
1298
1289
  if (!options.preserveTriggers && isBootstrapError && (previous.versionId === null || options.allowFirstDeploymentBootstrap === true || await previousIsSecretOnlyDraft())) return bootstrapFirstDeployment({
1299
1290
  ...options,
1300
1291
  message
1301
- }, commonArgs, previous.versionId);
1292
+ }, configPath, previous.versionId, false, previousTriggerConfig);
1302
1293
  return {
1303
1294
  ok: false,
1304
1295
  reason: "upload-failed",
1305
1296
  status: upload.status,
1306
1297
  ...commandDiagnosticFields(upload),
1307
1298
  ...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
1308
- ...isContainersUnavailable(output) ? { containersUnavailable: true } : {}
1299
+ ...isContainersUnavailable(upload) ? { containersUnavailable: true } : {}
1309
1300
  };
1310
1301
  }
1311
- const output = `${upload.stdout}\n${upload.stderr}`;
1312
- const versionId = output.match(VERSION_ID_RE)?.[1];
1302
+ const uploaded = readUploadResult(upload);
1303
+ const versionId = uploaded?.versionId;
1313
1304
  if (!versionId) return {
1314
1305
  ok: false,
1315
1306
  reason: "upload-output-invalid"
1316
1307
  };
1317
1308
  options.onProgress?.("Verifying the uploaded Worker...");
1318
1309
  if (inheritanceBaseline) {
1319
- const recent = await readRecentCloudflareVersionIds(options, commonArgs);
1310
+ const recent = await readRecentCloudflareVersionIds(options, configPath);
1320
1311
  const previousLatest = inheritanceBaseline.at(-1);
1321
1312
  const expected = [...inheritanceBaseline.map((version) => version.id), versionId].slice(-10);
1322
1313
  if (recent === null) return {
@@ -1341,7 +1332,15 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1341
1332
  if (!options.validateUploadedVersion && !inheritanceBaseline) throw new Error("A Worker candidate cannot be recorded without validating its upload and version sequence.");
1342
1333
  await options.recordVerifiedCandidate(versionId, previous.versionId);
1343
1334
  }
1344
- const reportedPreviewUrl = output.match(PREVIEW_URL_RE)?.[1];
1335
+ const reportedPreviewUrl = desiredTriggerConfig.preview_urls || desiredTriggerConfig.preservePreviewUrls || desiredTriggerConfig.preserve ? uploaded?.previewUrl : void 0;
1336
+ if (options.requirePreviewForReadiness && (!reportedPreviewUrl || !isCandidatePreviewUrl(reportedPreviewUrl, options.workerName, versionId, alias))) return {
1337
+ ok: false,
1338
+ reason: "preview-unavailable",
1339
+ versionId,
1340
+ previousVersionId,
1341
+ rollback: "not-needed",
1342
+ diagnostic: "This existing Worker has no version metadata binding and no genuine candidate preview URL. Keep its bindings unchanged; configure version metadata on the existing Worker or use an admitted version preview, then retry the handoff. Production traffic is unchanged."
1343
+ };
1345
1344
  let authenticatedWorkersDevSubdomain = reportedPreviewUrl ? extractWorkersDevSubdomain(reportedPreviewUrl) : void 0;
1346
1345
  let readinessUrl = reportedPreviewUrl;
1347
1346
  let readinessUsesCustomRoute = false;
@@ -1350,7 +1349,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1350
1349
  if (!options.preserveTriggers && (previous.versionId === null || options.allowFirstDeploymentBootstrap === true || await previousIsSecretOnlyDraft())) return bootstrapFirstDeployment({
1351
1350
  ...options,
1352
1351
  message
1353
- }, commonArgs, previous.versionId);
1352
+ }, configPath, previous.versionId, false, previousTriggerConfig);
1354
1353
  readinessUrl = desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes);
1355
1354
  readinessUsesCustomRoute = !desiredTriggerConfig.workers_dev && readinessUrl !== void 0;
1356
1355
  if (!readinessUrl) return {
@@ -1368,11 +1367,11 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1368
1367
  previousVersionId: previous.versionId,
1369
1368
  rollback: "not-needed"
1370
1369
  };
1371
- const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
1370
+ const stagingOwnerFailure = await verifyReadinessStagingOwner(options, configPath, previous.versionId, versionId);
1372
1371
  if (stagingOwnerFailure) return stagingOwnerFailure;
1373
- const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
1372
+ const staged = await deployVersionPercentages(options, configPath, [`${previous.versionId}@100`, `${versionId}@0`], message);
1374
1373
  if (!isCleanCommandExit(staged)) {
1375
- const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
1374
+ const cloudflareErrorCode = staged.errorCode;
1376
1375
  return {
1377
1376
  ok: false,
1378
1377
  reason: "readiness-staging-failed",
@@ -1387,8 +1386,8 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1387
1386
  stagedVersionOverride = true;
1388
1387
  }
1389
1388
  options.onProgress?.("Waiting for the Worker to become ready...");
1390
- let ready = await probeReadiness(`${readinessUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, readinessUsesCustomRoute ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, stagedVersionOverride ? versionId : void 0, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 6);
1391
- if (ready === "access-denied" && !stagedVersionOverride && previous.versionId !== null) {
1389
+ let ready = await probeReadiness(`${readinessUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, readinessUsesCustomRoute ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, stagedVersionOverride ? versionId : void 0, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 6, Number.POSITIVE_INFINITY, options.requireVersionMetadata ? versionId : void 0);
1390
+ if (ready === "access-denied" && !stagedVersionOverride && previous.versionId !== null && !options.requirePreviewForReadiness) {
1392
1391
  const stableReadinessUrl = desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, extractWorkersDevSubdomain(readinessUrl) ?? options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes);
1393
1392
  if (stableReadinessUrl && stableReadinessUrl !== readinessUrl) {
1394
1393
  if (!options.verifyUploader()) return {
@@ -1398,11 +1397,11 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1398
1397
  previousVersionId: previous.versionId,
1399
1398
  rollback: "not-needed"
1400
1399
  };
1401
- const stagingOwnerFailure = await verifyReadinessStagingOwner(options, commonArgs, previous.versionId, versionId);
1400
+ const stagingOwnerFailure = await verifyReadinessStagingOwner(options, configPath, previous.versionId, versionId);
1402
1401
  if (stagingOwnerFailure) return stagingOwnerFailure;
1403
- const staged = await deployVersionPercentages(options, commonArgs, [`${previous.versionId}@100`, `${versionId}@0`], message);
1402
+ const staged = await deployVersionPercentages(options, configPath, [`${previous.versionId}@100`, `${versionId}@0`], message);
1404
1403
  if (!isCleanCommandExit(staged)) {
1405
- const cloudflareErrorCode = parseCloudflareErrorCode(`${staged.stdout}\n${staged.stderr}`);
1404
+ const cloudflareErrorCode = staged.errorCode;
1406
1405
  return {
1407
1406
  ok: false,
1408
1407
  reason: "readiness-staging-failed",
@@ -1418,7 +1417,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1418
1417
  authenticatedWorkersDevSubdomain = extractWorkersDevSubdomain(stableReadinessUrl);
1419
1418
  readinessUsesCustomRoute = !desiredTriggerConfig.workers_dev;
1420
1419
  stagedVersionOverride = true;
1421
- ready = await probeReadiness(`${readinessUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, readinessUsesCustomRoute ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, versionId, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 6);
1420
+ ready = await probeReadiness(`${readinessUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, readinessUsesCustomRoute ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, versionId, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? 6, Number.POSITIVE_INFINITY, options.requireVersionMetadata ? versionId : void 0);
1422
1421
  }
1423
1422
  }
1424
1423
  if (ready !== "ready") return {
@@ -1426,16 +1425,16 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1426
1425
  reason: ready === "access-denied" ? "readiness-access-denied" : "readiness-failed",
1427
1426
  versionId,
1428
1427
  previousVersionId: previous.versionId,
1429
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, commonArgs, previous.versionId, "readiness failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1428
+ rollback: stagedVersionOverride ? await restorePreviousVersion(options, configPath, previous.versionId, "readiness failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1430
1429
  };
1431
1430
  if (!options.verifyUploader()) return {
1432
1431
  ok: false,
1433
1432
  reason: "uploader-changed",
1434
1433
  versionId,
1435
1434
  previousVersionId: previous.versionId,
1436
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, commonArgs, previous.versionId, "uploader verification failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1435
+ rollback: stagedVersionOverride ? await restorePreviousVersion(options, configPath, previous.versionId, "uploader verification failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1437
1436
  };
1438
- const ownerBeforeActivation = await readCurrentProductionVersion(options, commonArgs);
1437
+ const ownerBeforeActivation = await readCurrentProductionVersion(options, configPath);
1439
1438
  if (ownerBeforeActivation.kind === "failed") return {
1440
1439
  ok: false,
1441
1440
  reason: "status-failed",
@@ -1463,14 +1462,14 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1463
1462
  triggerRepair: "succeeded"
1464
1463
  };
1465
1464
  options.onProgress?.("Activating the Worker...");
1466
- const activate = await deployVersionPercentages(options, commonArgs, [`${versionId}@100`], message);
1465
+ const activate = await deployVersionPercentages(options, configPath, [`${versionId}@100`], message);
1467
1466
  if (!isCleanCommandExit(activate)) return {
1468
1467
  ok: false,
1469
1468
  reason: "activation-failed",
1470
1469
  status: activate.status,
1471
1470
  versionId,
1472
1471
  previousVersionId: previous.versionId,
1473
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, commonArgs, previous.versionId, "version activation failed", [stagedVersionPercentages(previous.versionId, versionId), [{
1472
+ rollback: stagedVersionOverride ? await restorePreviousVersion(options, configPath, previous.versionId, "version activation failed", [stagedVersionPercentages(previous.versionId, versionId), [{
1474
1473
  versionId,
1475
1474
  percentage: 100
1476
1475
  }]]) : "not-needed"
@@ -1483,7 +1482,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1483
1482
  rollback: previous.versionId ? "unsafe" : "unavailable"
1484
1483
  };
1485
1484
  if (options.preserveTriggers) {
1486
- const owner = await readCurrentProductionVersion(options, commonArgs);
1485
+ const owner = await readCurrentProductionVersion(options, configPath);
1487
1486
  if (owner.kind !== "ok" || owner.versionId !== versionId) return {
1488
1487
  ok: false,
1489
1488
  reason: "deployment-changed",
@@ -1534,7 +1533,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1534
1533
  previousVersionId: previous.versionId,
1535
1534
  rollback: "unsafe"
1536
1535
  };
1537
- const ownerBeforeCompensation = await readCurrentProductionVersion(options, commonArgs);
1536
+ const ownerBeforeCompensation = await readCurrentProductionVersion(options, configPath);
1538
1537
  if (ownerBeforeCompensation.kind !== "ok" || ownerBeforeCompensation.versionId === null) return {
1539
1538
  ok: false,
1540
1539
  reason: "triggers-failed",
@@ -1566,7 +1565,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1566
1565
  triggerRepair: repair.ok ? "succeeded" : repair.reason === "metadata-unavailable" ? "unavailable" : "failed"
1567
1566
  };
1568
1567
  }
1569
- const rollback = await restorePreviousVersionAndTriggers(options, commonArgs, previous.versionId, "trigger activation failed", desiredTriggerConfig, versionId, versionedPreviousTriggerConfigWithTraffic);
1568
+ const rollback = await restorePreviousVersionAndTriggers(options, configPath, previous.versionId, "trigger activation failed", desiredTriggerConfig, versionId, versionedPreviousTriggerConfigWithTraffic);
1570
1569
  return {
1571
1570
  ok: false,
1572
1571
  reason: "triggers-failed",
@@ -1599,8 +1598,8 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1599
1598
  ...workersDevSubdomainResult(readinessUrl)
1600
1599
  };
1601
1600
  }
1602
- async function restorePreviousVersionAndTriggers(options, commonArgs, previousVersionId, cause, failedTriggerConfig, failedVersionId, capturedTriggerConfig) {
1603
- const traffic = await restorePreviousVersion(options, commonArgs, previousVersionId, cause, [[{
1601
+ async function restorePreviousVersionAndTriggers(options, configPath, previousVersionId, cause, failedTriggerConfig, failedVersionId, capturedTriggerConfig) {
1602
+ const traffic = await restorePreviousVersion(options, configPath, previousVersionId, cause, [[{
1604
1603
  versionId: failedVersionId,
1605
1604
  percentage: 100
1606
1605
  }]]);
@@ -1608,7 +1607,7 @@ async function restorePreviousVersionAndTriggers(options, commonArgs, previousVe
1608
1607
  if (!options.verifyUploader()) return "unsafe";
1609
1608
  let targetTriggerConfig = capturedTriggerConfig;
1610
1609
  if (!targetTriggerConfig) {
1611
- const metadata = await readCloudflareVersionMetadata(options, commonArgs, previousVersionId);
1610
+ const metadata = await readCloudflareVersionMetadata(options, configPath, previousVersionId);
1612
1611
  if (metadata?.schema !== 2) return "unavailable";
1613
1612
  targetTriggerConfig = metadata.triggerConfig;
1614
1613
  }
@@ -1627,23 +1626,20 @@ async function restorePreviousVersionAndTriggers(options, commonArgs, previousVe
1627
1626
  });
1628
1627
  return triggers.ok && triggers.targetOwnsTraffic ? "succeeded" : "failed";
1629
1628
  }
1630
- async function readCloudflareVersionMetadata(options, commonArgs, versionId) {
1631
- return (await readCloudflareVersionInspection(options, commonArgs, versionId))?.metadata ?? null;
1629
+ async function readCloudflareVersionMetadata(options, configPath, versionId) {
1630
+ return (await readCloudflareVersionInspection(options, configPath, versionId))?.metadata ?? null;
1632
1631
  }
1633
- async function readCloudflareVersionInspection(options, commonArgs, versionId, knownVersions) {
1632
+ async function readCloudflareVersionInspection(options, configPath, versionId, knownVersions) {
1634
1633
  try {
1635
1634
  let versions = knownVersions;
1636
1635
  if (versions === void 0) {
1637
- const result = await options.runWrangler([
1638
- "versions",
1639
- "list",
1640
- ...commonArgs,
1641
- "--name",
1642
- options.workerName,
1643
- "--json"
1644
- ], { capture: true });
1636
+ const result = await options.runWrangler({
1637
+ type: "versions.list",
1638
+ configPath,
1639
+ workerName: options.workerName
1640
+ }, { capture: true });
1645
1641
  if (!isCleanCommandExit(result)) return null;
1646
- versions = JSON.parse(result.stdout);
1642
+ versions = result.data;
1647
1643
  }
1648
1644
  if (!Array.isArray(versions)) return null;
1649
1645
  for (const entry of versions) {
@@ -1652,17 +1648,14 @@ async function readCloudflareVersionInspection(options, commonArgs, versionId, k
1652
1648
  const message = annotations?.["workers/message"];
1653
1649
  let draft = entry;
1654
1650
  if (annotations?.["workers/triggered_by"] === "secret" && Object.keys(annotations).length === 1 && !isRecord(entry.resources)) {
1655
- const inspection = await Promise.resolve(options.runWrangler([
1656
- "versions",
1657
- "view",
1651
+ const inspection = await Promise.resolve(options.runWrangler({
1652
+ type: "versions.view",
1658
1653
  versionId,
1659
- ...commonArgs,
1660
- "--name",
1661
- options.workerName,
1662
- "--json"
1663
- ], { capture: true }));
1654
+ configPath,
1655
+ workerName: options.workerName
1656
+ }, { capture: true }));
1664
1657
  if (isCleanCommandExit(inspection)) {
1665
- const inspected = JSON.parse(inspection.stdout);
1658
+ const inspected = inspection.data;
1666
1659
  if (isRecord(inspected) && inspected.id === versionId) draft = inspected;
1667
1660
  }
1668
1661
  }
@@ -1676,21 +1669,18 @@ async function readCloudflareVersionInspection(options, commonArgs, versionId, k
1676
1669
  }
1677
1670
  return null;
1678
1671
  }
1679
- async function readRecentCloudflareVersionIds(options, commonArgs) {
1680
- const result = await Promise.resolve(options.runWrangler([
1681
- "versions",
1682
- "list",
1683
- ...commonArgs,
1684
- "--name",
1685
- options.workerName,
1686
- "--json"
1687
- ], { capture: true }));
1672
+ async function readRecentCloudflareVersionIds(options, configPath) {
1673
+ const result = await Promise.resolve(options.runWrangler({
1674
+ type: "versions.list",
1675
+ configPath,
1676
+ workerName: options.workerName
1677
+ }, { capture: true }));
1688
1678
  if (!isCleanCommandExit(result)) {
1689
- if (isMissingWorkerOutput(`${result.stdout}\n${result.stderr}`)) return [];
1679
+ if (isCompletedCommand(result) && (result.failure === "not-found" || result.errorCode === 10007)) return [];
1690
1680
  return null;
1691
1681
  }
1692
1682
  try {
1693
- const versions = JSON.parse(result.stdout);
1683
+ const versions = result.data;
1694
1684
  if (!Array.isArray(versions) || !versions.every((version) => isRecord(version) && typeof version.id === "string" && typeof version.number === "number" && Number.isSafeInteger(version.number))) return null;
1695
1685
  return versions.map((version) => ({
1696
1686
  id: version.id,
@@ -1704,29 +1694,26 @@ async function readRecentCloudflareVersionIds(options, commonArgs) {
1704
1694
  function arraysEqual(left, right) {
1705
1695
  return left.length === right.length && left.every((value, index) => value === right[index]);
1706
1696
  }
1707
- async function candidateReplacesAllRemoteSecrets(options, commonArgs, activeVersionId, latestVersionId) {
1697
+ async function candidateReplacesAllRemoteSecrets(options, configPath, activeVersionId, latestVersionId) {
1708
1698
  const versionIds = [...new Set([activeVersionId, latestVersionId].filter(Boolean))];
1709
1699
  const secretBindings = [];
1710
1700
  for (const versionId of versionIds) {
1711
- const bindings = await readCloudflareVersionSecretBindings(options, commonArgs, versionId);
1701
+ const bindings = await readCloudflareVersionSecretBindings(options, configPath, versionId);
1712
1702
  if (bindings === null) return null;
1713
1703
  secretBindings.push(bindings);
1714
1704
  }
1715
1705
  return secretBindings.every((bindings) => bindings.every((binding) => binding.type === "secret_text" && Object.hasOwn(options.candidateSecrets ?? {}, binding.name)));
1716
1706
  }
1717
- async function readCloudflareVersionSecretBindings(options, commonArgs, versionId) {
1718
- const result = await Promise.resolve(options.runWrangler([
1719
- "versions",
1720
- "view",
1707
+ async function readCloudflareVersionSecretBindings(options, configPath, versionId) {
1708
+ const result = await Promise.resolve(options.runWrangler({
1709
+ type: "versions.view",
1721
1710
  versionId,
1722
- ...commonArgs,
1723
- "--name",
1724
- options.workerName,
1725
- "--json"
1726
- ], { capture: true }));
1711
+ configPath,
1712
+ workerName: options.workerName
1713
+ }, { capture: true }));
1727
1714
  if (!isCleanCommandExit(result)) return null;
1728
1715
  try {
1729
- const version = JSON.parse(result.stdout);
1716
+ const version = result.data;
1730
1717
  if (!isRecord(version) || version.id !== versionId || !isRecord(version.resources)) return null;
1731
1718
  const bindings = version.resources.bindings;
1732
1719
  if (!Array.isArray(bindings) || !bindings.every(isRecord)) return null;
@@ -1750,13 +1737,13 @@ async function readCloudflareVersionSecretBindings(options, commonArgs, versionI
1750
1737
  * brand-new Worker can therefore need one ordinary deploy before later releases use preview-first
1751
1738
  * version activation. This fallback requires prior proof that no application had been deployed.
1752
1739
  */
1753
- async function bootstrapFirstDeployment(options, commonArgs, previousVersionId, atomic = false) {
1740
+ async function bootstrapFirstDeployment(options, configPath, previousVersionId, atomic = false, previousTriggerConfig) {
1754
1741
  if (!options.verifyUploader()) return {
1755
1742
  ok: false,
1756
1743
  reason: "uploader-changed"
1757
1744
  };
1758
1745
  if (atomic) {
1759
- const current = await readCurrentProductionVersion(options, commonArgs);
1746
+ const current = await readCurrentProductionVersion(options, configPath);
1760
1747
  if (current.kind === "failed") return {
1761
1748
  ok: false,
1762
1749
  reason: "status-failed",
@@ -1770,53 +1757,114 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId,
1770
1757
  };
1771
1758
  }
1772
1759
  options.onProgress?.(atomic ? "Publishing the Worker atomically..." : "Publishing the first Worker deployment...");
1773
- const bootstrap = await Promise.resolve(options.runWrangler([
1774
- "deploy",
1775
- ...commonArgs,
1776
- "--name",
1777
- options.workerName,
1778
- "--message",
1779
- options.message,
1780
- ...options.candidateSecretsFilePath ? ["--secrets-file", options.candidateSecretsFilePath] : [],
1781
- ...options.assetsDirectoryOverride ? ["--assets", options.assetsDirectoryOverride] : [],
1782
- "--strict",
1783
- "--experimental-provision=false",
1784
- "--experimental-auto-create=false",
1785
- "--autoconfig=false"
1786
- ], { capture: true }));
1787
- const output = `${bootstrap.stdout}\n${bootstrap.stderr}`;
1760
+ const bootstrap = await Promise.resolve(options.runWrangler({
1761
+ type: "worker.deploy",
1762
+ configPath,
1763
+ workerName: options.workerName,
1764
+ message: options.message,
1765
+ secretsFilePath: options.candidateSecretsFilePath,
1766
+ assetsDirectory: options.assetsDirectoryOverride
1767
+ }, { capture: true }));
1788
1768
  if (!isCleanCommandExit(bootstrap)) {
1789
- const cloudflareErrorCode = parseCloudflareErrorCode(output);
1769
+ const cloudflareErrorCode = bootstrap.errorCode;
1790
1770
  return {
1791
1771
  ok: false,
1792
1772
  reason: atomic ? "atomic-failed" : "bootstrap-failed",
1793
1773
  status: bootstrap.status,
1794
1774
  ...commandDiagnosticFields(bootstrap),
1795
1775
  ...cloudflareErrorCode === void 0 ? {} : { cloudflareErrorCode },
1796
- ...isContainersUnavailable(output) ? { containersUnavailable: true } : {}
1776
+ ...isContainersUnavailable(bootstrap) ? { containersUnavailable: true } : {}
1797
1777
  };
1798
1778
  }
1799
- const versionId = output.match(VERSION_ID_RE)?.[1];
1779
+ const published = readUploadResult(bootstrap);
1780
+ const versionId = published?.versionId;
1800
1781
  const desiredMetadata = parseCloudflareVersionMessage(options.message);
1801
1782
  const desiredTriggerConfig = desiredMetadata?.schema === 2 ? desiredMetadata.triggerConfig : options.configPath ? readCloudflareTriggerConfig(options.configPath) : triggerConfigFromWrangler({});
1802
- const reportedUrl = output.match(PREVIEW_URL_RE)?.[1] ?? output.match(WORKERS_DEV_URL_RE)?.[1];
1803
- const reportedOrConfiguredUrl = reportedUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1783
+ const reportedOrConfiguredUrl = published?.previewUrl ?? published?.workersDevUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1804
1784
  if (!versionId || !reportedOrConfiguredUrl) return {
1805
1785
  ok: false,
1806
1786
  reason: atomic ? "atomic-output-invalid" : "bootstrap-output-invalid",
1807
1787
  previousVersionId,
1808
1788
  ...versionId ? { versionId } : {}
1809
1789
  };
1790
+ if (!options.verifyUploader()) return {
1791
+ ok: false,
1792
+ reason: "uploader-changed",
1793
+ versionId,
1794
+ previousVersionId,
1795
+ rollback: "unsafe"
1796
+ };
1797
+ options.onProgress?.("Updating routes and triggers...");
1798
+ const triggerOptions = {
1799
+ runWrangler: options.runWrangler,
1800
+ workerName: options.workerName,
1801
+ configPath: options.configPath,
1802
+ targetVersionId: versionId,
1803
+ targetTriggerConfig: options.preserveTriggers ? {
1804
+ ...desiredTriggerConfig,
1805
+ preserve: true
1806
+ } : desiredTriggerConfig,
1807
+ previousTriggerConfig,
1808
+ ...previousVersionId && previousTriggerConfig ? { knownTriggerConfigs: { [previousVersionId]: previousTriggerConfig } } : {},
1809
+ publishEmptyCustomDomains: options.publishEmptyCustomDomains,
1810
+ publishEmptyCrons: options.publishEmptyCrons
1811
+ };
1812
+ const triggers = await stabilizeCloudflareVersionTriggers(triggerOptions);
1813
+ if (!triggers.ok || !triggers.targetOwnsTraffic) {
1814
+ const superseded = triggers.activeVersionId !== void 0 && triggers.activeVersionId !== null && triggers.activeVersionId !== versionId;
1815
+ return {
1816
+ ok: false,
1817
+ reason: superseded ? "deployment-changed" : "triggers-failed",
1818
+ ...!triggers.ok ? { status: triggers.status } : {},
1819
+ versionId,
1820
+ previousVersionId,
1821
+ activeVersionId: triggers.activeVersionId,
1822
+ rollback: superseded ? "not-needed" : triggers.activeVersionId ? "unavailable" : "unsafe",
1823
+ ...superseded ? { triggerRepair: triggers.ok ? "succeeded" : triggers.reason === "metadata-unavailable" ? "unavailable" : "failed" } : {}
1824
+ };
1825
+ }
1810
1826
  const previewUrl = publishedCloudflareUrl(options.workerName, desiredTriggerConfig, reportedOrConfiguredUrl);
1827
+ const usesCustomDomain = !desiredTriggerConfig.workers_dev && desiredTriggerConfig.routes.some((route) => isRecord(route) && route.custom_domain === true);
1828
+ const customDomainBootstrap = !atomic && usesCustomDomain;
1829
+ const readinessTimeoutMs = customDomainBootstrap ? 3e5 : 6e4;
1811
1830
  options.onProgress?.("Waiting for the Worker to become ready...");
1812
- 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);
1831
+ const ready = await probeReadiness(`${previewUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, extractWorkersDevSubdomain(previewUrl) ? options.readinessHeaders : void 0, extractWorkersDevSubdomain(previewUrl), void 0, options.fetch ?? globalThis.fetch, options.sleep ?? defaultSleep, options.readinessAttempts ?? (customDomainBootstrap ? 150 : 30), readinessTimeoutMs, options.requireVersionMetadata ? versionId : void 0);
1813
1832
  if (ready !== "ready") return {
1814
1833
  ok: false,
1815
1834
  reason: ready === "access-denied" ? atomic ? "atomic-readiness-access-denied" : "bootstrap-readiness-access-denied" : atomic ? "atomic-readiness-failed" : "bootstrap-readiness-failed",
1816
1835
  versionId,
1817
1836
  previousVersionId,
1818
- rollback: "unavailable"
1837
+ rollback: "unavailable",
1838
+ ...ready === "failed" && usesCustomDomain ? { diagnostic: "The Custom Domain may still be propagating DNS or its TLS certificate. Check the configured domain in Cloudflare, then retry `void deploy --platform cloudflare` once its /__void/ready endpoint is reachable." } : {}
1819
1839
  };
1840
+ if (!options.verifyUploader()) return {
1841
+ ok: false,
1842
+ reason: "uploader-changed",
1843
+ versionId,
1844
+ previousVersionId,
1845
+ rollback: "unsafe"
1846
+ };
1847
+ const ownerAfterReadiness = await readCurrentProductionVersion(options, configPath);
1848
+ if (ownerAfterReadiness.kind === "failed") return {
1849
+ ok: false,
1850
+ reason: "status-failed",
1851
+ status: ownerAfterReadiness.status,
1852
+ versionId,
1853
+ previousVersionId,
1854
+ rollback: "unsafe"
1855
+ };
1856
+ if (ownerAfterReadiness.kind !== "ok" || ownerAfterReadiness.versionId !== versionId) {
1857
+ const repair = await stabilizeCloudflareVersionTriggers(triggerOptions);
1858
+ return {
1859
+ ok: false,
1860
+ reason: "deployment-changed",
1861
+ versionId,
1862
+ previousVersionId,
1863
+ activeVersionId: repair.activeVersionId,
1864
+ rollback: "not-needed",
1865
+ triggerRepair: repair.ok ? "succeeded" : repair.reason === "metadata-unavailable" ? "unavailable" : "failed"
1866
+ };
1867
+ }
1820
1868
  return {
1821
1869
  ok: true,
1822
1870
  versionId,
@@ -1830,32 +1878,36 @@ async function bootstrapFirstDeployment(options, commonArgs, previousVersionId,
1830
1878
  ...workersDevSubdomainResult(previewUrl)
1831
1879
  };
1832
1880
  }
1833
- function parseCloudflareErrorCode(output) {
1834
- const value = output.match(CLOUDFLARE_ERROR_CODE_RE)?.[1];
1835
- return value === void 0 ? void 0 : Number(value);
1881
+ function readUploadResult(result) {
1882
+ const data = result.data;
1883
+ if (!isRecord(data) || typeof data.versionId !== "string" || !/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(data.versionId)) return null;
1884
+ if (data.previewUrl !== void 0 && typeof data.previewUrl !== "string" || data.workersDevUrl !== void 0 && typeof data.workersDevUrl !== "string") return null;
1885
+ return data;
1886
+ }
1887
+ function versionPercentages(values) {
1888
+ return values.map((value) => {
1889
+ const [versionId, percentage] = value.split("@");
1890
+ return {
1891
+ versionId,
1892
+ percentage: Number(percentage)
1893
+ };
1894
+ });
1836
1895
  }
1837
1896
  function commandDiagnosticFields(result) {
1838
1897
  const diagnostic = cloudflareCommandDiagnostic(result);
1839
1898
  return diagnostic ? { diagnostic } : {};
1840
1899
  }
1841
- function isMissingWorkerOutput(output) {
1842
- return parseCloudflareErrorCode(output) === 10007 || /no deployments|\bworker\b.*\b(?:does not exist|not found)\b/i.test(output);
1843
- }
1844
- /** Recognize the Containers account-entitlement failure Wrangler exposes during upload. */
1845
- function isContainersUnavailable(output) {
1846
- return /\/containers\/me(?:\b|\?)/i.test(output) && /\b403\b/.test(output) || /Cloudflare Containers[^\n]*(?:Workers Paid|not (?:enabled|available)|subscription)/i.test(output);
1847
- }
1848
- async function readCurrentProductionVersion(options, commonArgs) {
1849
- const result = await Promise.resolve(options.runWrangler([
1850
- "deployments",
1851
- "status",
1852
- ...commonArgs,
1853
- "--name",
1854
- options.workerName,
1855
- "--json"
1856
- ], { capture: true }));
1900
+ function isContainersUnavailable(result) {
1901
+ return result.failure === "containers-unavailable" || (result.errors?.some(({ message }) => /Cloudflare Containers[^\n]*(?:Workers Paid|not (?:enabled|available)|subscription)/i.test(message) || /\/containers\/me(?:\b|\?)/i.test(message) && /\b403\b/.test(message)) ?? false);
1902
+ }
1903
+ async function readCurrentProductionVersion(options, configPath) {
1904
+ const result = await Promise.resolve(options.runWrangler({
1905
+ type: "deployments.status",
1906
+ configPath,
1907
+ workerName: options.workerName
1908
+ }, { capture: true }));
1857
1909
  if (!isCleanCommandExit(result)) {
1858
- if (isMissingWorkerOutput(`${result.stdout}\n${result.stderr}`)) return {
1910
+ if (isCompletedCommand(result) && (result.failure === "not-found" || result.errorCode === 10007)) return {
1859
1911
  kind: "ok",
1860
1912
  versionId: null,
1861
1913
  versions: []
@@ -1866,7 +1918,7 @@ async function readCurrentProductionVersion(options, commonArgs) {
1866
1918
  };
1867
1919
  }
1868
1920
  try {
1869
- const versions = (JSON.parse(result.stdout).versions ?? []).map((version) => ({
1921
+ const versions = (result.data.versions ?? []).map((version) => ({
1870
1922
  versionId: typeof version.version_id === "string" ? version.version_id : "",
1871
1923
  percentage: Number(version.percentage)
1872
1924
  }));
@@ -1900,8 +1952,8 @@ function stagedVersionPercentages(previousVersionId, versionId) {
1900
1952
  percentage: 0
1901
1953
  }] : [];
1902
1954
  }
1903
- async function verifyReadinessStagingOwner(options, commonArgs, previousVersionId, versionId) {
1904
- const owner = await readCurrentProductionVersion(options, commonArgs);
1955
+ async function verifyReadinessStagingOwner(options, configPath, previousVersionId, versionId) {
1956
+ const owner = await readCurrentProductionVersion(options, configPath);
1905
1957
  if (owner.kind === "failed") return {
1906
1958
  ok: false,
1907
1959
  reason: "status-failed",
@@ -1931,7 +1983,7 @@ async function verifyReadinessStagingOwner(options, commonArgs, previousVersionI
1931
1983
  };
1932
1984
  return null;
1933
1985
  }
1934
- async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY) {
1986
+ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY, expectedVersionId) {
1935
1987
  const headers = guardedReadinessHeaders(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain);
1936
1988
  if (headers === null) return "failed";
1937
1989
  const deadline = Date.now() + timeoutMs;
@@ -1950,7 +2002,8 @@ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWo
1950
2002
  signal: AbortSignal.timeout(Math.max(1, Math.min(1e4, remaining)))
1951
2003
  });
1952
2004
  if (response.ok) {
1953
- if ((await response.json()).ready === true) return "ready";
2005
+ const body = await response.json();
2006
+ if (body.ready === true && (expectedVersionId === void 0 || body.versionId === expectedVersionId)) return "ready";
1954
2007
  } else if (await isCloudflareAccessDenial(response)) return "access-denied";
1955
2008
  } catch {} finally {
1956
2009
  if (response && !response.bodyUsed) await response.body?.cancel().catch(() => {});
@@ -1986,7 +2039,7 @@ async function isCloudflareAccessDenial(response) {
1986
2039
  return false;
1987
2040
  }
1988
2041
  }
1989
- async function deployVersionPercentages(options, commonArgs, percentages, message) {
2042
+ async function deployVersionPercentages(options, configPath, percentages, message) {
1990
2043
  const metadata = parseCloudflareVersionMessage(message);
1991
2044
  if ((options.preserveTriggers || metadata?.schema === 2 && metadata.triggerConfig.preserve) && options.configPath) return deployCloudflareVersionTraffic({
1992
2045
  ...options,
@@ -1994,17 +2047,13 @@ async function deployVersionPercentages(options, commonArgs, percentages, messag
1994
2047
  percentages,
1995
2048
  message
1996
2049
  });
1997
- return options.runWrangler([
1998
- "versions",
1999
- "deploy",
2000
- ...percentages,
2001
- ...commonArgs,
2002
- "--name",
2003
- options.workerName,
2004
- "--message",
2005
- message,
2006
- "--yes"
2007
- ], { capture: false });
2050
+ return options.runWrangler({
2051
+ type: "versions.deploy",
2052
+ versions: versionPercentages(percentages),
2053
+ configPath,
2054
+ workerName: options.workerName,
2055
+ message
2056
+ }, { capture: false });
2008
2057
  }
2009
2058
  /** Change version traffic without synchronizing Worker settings from local config. */
2010
2059
  async function deployCloudflareVersionTraffic(options) {
@@ -2013,19 +2062,13 @@ async function deployCloudflareVersionTraffic(options) {
2013
2062
  const path = join(directory, "wrangler.json");
2014
2063
  try {
2015
2064
  writeFileSync(path, JSON.stringify(config), { mode: 384 });
2016
- return await options.runWrangler([
2017
- "versions",
2018
- "deploy",
2019
- ...options.percentages,
2020
- "-c",
2021
- path,
2022
- "--env=",
2023
- "--name",
2024
- options.workerName,
2025
- "--message",
2026
- options.message,
2027
- "--yes"
2028
- ], { capture: false });
2065
+ return await options.runWrangler({
2066
+ type: "versions.deploy",
2067
+ versions: versionPercentages(options.percentages),
2068
+ configPath: path,
2069
+ workerName: options.workerName,
2070
+ message: options.message
2071
+ }, { capture: false });
2029
2072
  } finally {
2030
2073
  rmSync(directory, {
2031
2074
  recursive: true,
@@ -2033,12 +2076,12 @@ async function deployCloudflareVersionTraffic(options) {
2033
2076
  });
2034
2077
  }
2035
2078
  }
2036
- async function restorePreviousVersion(options, commonArgs, previousVersionId, cause, expectedOwnedTraffic) {
2079
+ async function restorePreviousVersion(options, configPath, previousVersionId, cause, expectedOwnedTraffic) {
2037
2080
  if (!previousVersionId) return "unavailable";
2038
2081
  if (!options.verifyUploader()) return "unsafe";
2039
- const owner = await readCurrentProductionVersion(options, commonArgs);
2082
+ const owner = await readCurrentProductionVersion(options, configPath);
2040
2083
  if (owner.kind !== "ok" || !expectedOwnedTraffic.some((expected) => productionVersionPercentagesEqual(owner.versions, expected))) return "unsafe";
2041
- return isCleanCommandExit(await deployVersionPercentages(options, commonArgs, [`${previousVersionId}@100`], `Void automatic rollback after ${cause}.`)) ? "succeeded" : "failed";
2084
+ return isCleanCommandExit(await deployVersionPercentages(options, configPath, [`${previousVersionId}@100`], `Void automatic rollback after ${cause}.`)) ? "succeeded" : "failed";
2042
2085
  }
2043
2086
  function workersDevReadinessUrl(workerName, value) {
2044
2087
  const subdomain = normalizeWorkersDevSubdomain(value);
@@ -2072,8 +2115,11 @@ function workersDevSubdomainResult(url) {
2072
2115
  const workersDevSubdomain = extractWorkersDevSubdomain(url);
2073
2116
  return workersDevSubdomain ? { workersDevSubdomain } : {};
2074
2117
  }
2118
+ function isCompletedCommand(result) {
2119
+ return Number.isSafeInteger(result.status) && result.status >= 0 && result.signal == null && (result.error == null || result.error === "");
2120
+ }
2075
2121
  function isCleanCommandExit(result) {
2076
- return result.status === 0 && result.signal == null && (result.error == null || result.error === "");
2122
+ return result.status === 0 && isCompletedCommand(result);
2077
2123
  }
2078
2124
  function defaultSleep(milliseconds) {
2079
2125
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -2087,13 +2133,6 @@ function defaultSleep(milliseconds) {
2087
2133
  */
2088
2134
  function createCloudflareMetadataRunner(options) {
2089
2135
  let tokenPromise;
2090
- const common = [
2091
- ...options.configPath ? ["-c", options.configPath] : [],
2092
- "--env=",
2093
- "--name",
2094
- options.workerName,
2095
- "--json"
2096
- ];
2097
2136
  const prefix = `${options.apiBaseUrl.replace(/\/$/, "")}/accounts/${encodeURIComponent(options.accountId)}/workers/scripts/${encodeURIComponent(options.workerName)}`;
2098
2137
  const fetcher = options.fetch ?? globalThis.fetch;
2099
2138
  const proxyEnv = options.env ?? process.env;
@@ -2104,26 +2143,22 @@ function createCloudflareMetadataRunner(options) {
2104
2143
  stderr: "Cloudflare uploader integrity check failed.",
2105
2144
  error: "uploader-changed"
2106
2145
  };
2107
- const fallback = (args, runOptions) => options.verifyUploader() ? options.runWrangler(args, runOptions) : refused;
2108
- return async (args, runOptions) => {
2146
+ const fallback = (operation, runOptions) => options.verifyUploader() ? options.runWrangler(operation, runOptions) : refused;
2147
+ return async (operation, runOptions) => {
2109
2148
  let kind;
2110
2149
  let resource;
2111
- let rest = [];
2112
- if (args[0] === "deployments" && args[1] === "status") {
2150
+ if (operation.type === "deployments.status") {
2113
2151
  kind = "deployment";
2114
2152
  resource = "/deployments";
2115
- rest = args.slice(2);
2116
- } else if (args[0] === "versions" && args[1] === "list") {
2153
+ } else if (operation.type === "versions.list") {
2117
2154
  kind = "versions";
2118
2155
  resource = "/versions?deployable=true";
2119
- rest = args.slice(2);
2120
- } else if (args[0] === "versions" && args[1] === "view" && /^[0-9a-f-]{36}$/i.test(args[2] ?? "")) {
2156
+ } else if (operation.type === "versions.view" && /^[0-9a-f-]{36}$/i.test(operation.versionId)) {
2121
2157
  kind = "version";
2122
- resource = `/versions/${encodeURIComponent(args[2])}`;
2123
- rest = args.slice(3);
2158
+ resource = "/versions/" + encodeURIComponent(operation.versionId);
2124
2159
  }
2125
- if (!kind || !runOptions.capture || rest.length !== common.length || rest.some((value, i) => value !== common[i])) return fallback(args, runOptions);
2126
- if (requiresProxyTransport) return fallback(args, runOptions);
2160
+ if (!kind || !runOptions.capture || !("workerName" in operation) || operation.workerName !== options.workerName || operation.configPath !== options.configPath) return fallback(operation, runOptions);
2161
+ if (requiresProxyTransport) return fallback(operation, runOptions);
2127
2162
  let response;
2128
2163
  try {
2129
2164
  if (!tokenPromise && !options.verifyUploader()) return refused;
@@ -2144,29 +2179,30 @@ function createCloudflareMetadataRunner(options) {
2144
2179
  if (result.deployments.length === 0) return {
2145
2180
  status: 1,
2146
2181
  stdout: "",
2147
- stderr: `The Worker ${options.workerName} has no deployments.`
2182
+ stderr: `The Worker ${options.workerName} has no deployments.`,
2183
+ errorCode: 10007
2148
2184
  };
2149
2185
  if (record(latest) && Array.isArray(latest.versions)) return {
2150
2186
  status: 0,
2151
- stdout: JSON.stringify(latest),
2187
+ stdout: "",
2188
+ data: latest,
2152
2189
  stderr: ""
2153
2190
  };
2154
2191
  }
2155
- if (kind === "version" && result.id === args[2]) return {
2192
+ if (operation.type === "versions.view" && result.id === operation.versionId) return {
2156
2193
  status: 0,
2157
- stdout: JSON.stringify(result),
2194
+ stdout: "",
2195
+ data: result,
2158
2196
  stderr: ""
2159
2197
  };
2160
2198
  if (kind === "versions" && Array.isArray(result.items)) {
2161
2199
  const items = result.items;
2162
- if (items.every((item) => record(item) && record(item.metadata) && typeof item.metadata.created_on === "string")) {
2163
- const sorted = items.sort((a, b) => a.metadata.created_on.localeCompare(b.metadata.created_on)).slice(-10);
2164
- return {
2165
- status: 0,
2166
- stdout: JSON.stringify(sorted),
2167
- stderr: ""
2168
- };
2169
- }
2200
+ if (items.every((item) => record(item) && record(item.metadata) && typeof item.metadata.created_on === "string")) return {
2201
+ status: 0,
2202
+ stdout: "",
2203
+ data: items.sort((a, b) => a.metadata.created_on.localeCompare(b.metadata.created_on)).slice(-10),
2204
+ stderr: ""
2205
+ };
2170
2206
  }
2171
2207
  }
2172
2208
  }
@@ -2176,7 +2212,7 @@ function createCloudflareMetadataRunner(options) {
2176
2212
  } finally {
2177
2213
  if (response && !response.bodyUsed) await response.body?.cancel().catch(() => {});
2178
2214
  }
2179
- return fallback(args, runOptions);
2215
+ return fallback(operation, runOptions);
2180
2216
  };
2181
2217
  }
2182
2218
  function record(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/deploy-cloudflare",
3
- "version": "0.21.9",
3
+ "version": "0.23.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -20,15 +20,15 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@void/deploy-core": "0.21.9",
23
+ "@void/deploy-core": "0.23.0",
24
24
  "jsonc-parser": "3.3.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@cloudflare/workers-types": "^4.20260702.1",
28
- "@types/node": "^26.5.1",
28
+ "@types/node": "^26.6.3",
29
29
  "pathslash": "^0.1.0",
30
30
  "typescript": "7.0.2",
31
- "vite-plus": "0.3.1"
31
+ "vite-plus": "1.0.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "vp pack",