@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 +115 -2
- package/dist/index.mjs +313 -277
- package/package.json +4 -4
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 = (
|
|
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
|
-
|
|
120
|
-
options.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
options.
|
|
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 &&
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
153
|
-
|
|
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(
|
|
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 =
|
|
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 ?
|
|
484
|
-
|
|
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
|
-
|
|
839
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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 ||
|
|
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 =
|
|
1204
|
-
if (!(inheritedChain.length > 0 && inheritedChain.every((version, index) => index === 0 || version.number === inheritedChain[index - 1].number + 1) && (arraysEqual(inheritedCandidates,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
},
|
|
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
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
1296
|
-
const
|
|
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
|
-
},
|
|
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(
|
|
1299
|
+
...isContainersUnavailable(upload) ? { containersUnavailable: true } : {}
|
|
1309
1300
|
};
|
|
1310
1301
|
}
|
|
1311
|
-
const
|
|
1312
|
-
const versionId =
|
|
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,
|
|
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 =
|
|
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
|
-
},
|
|
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,
|
|
1370
|
+
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, configPath, previous.versionId, versionId);
|
|
1372
1371
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1373
|
-
const staged = await deployVersionPercentages(options,
|
|
1372
|
+
const staged = await deployVersionPercentages(options, configPath, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1374
1373
|
if (!isCleanCommandExit(staged)) {
|
|
1375
|
-
const cloudflareErrorCode =
|
|
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,
|
|
1400
|
+
const stagingOwnerFailure = await verifyReadinessStagingOwner(options, configPath, previous.versionId, versionId);
|
|
1402
1401
|
if (stagingOwnerFailure) return stagingOwnerFailure;
|
|
1403
|
-
const staged = await deployVersionPercentages(options,
|
|
1402
|
+
const staged = await deployVersionPercentages(options, configPath, [`${previous.versionId}@100`, `${versionId}@0`], message);
|
|
1404
1403
|
if (!isCleanCommandExit(staged)) {
|
|
1405
|
-
const cloudflareErrorCode =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1603
|
-
const traffic = await restorePreviousVersion(options,
|
|
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,
|
|
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,
|
|
1631
|
-
return (await readCloudflareVersionInspection(options,
|
|
1629
|
+
async function readCloudflareVersionMetadata(options, configPath, versionId) {
|
|
1630
|
+
return (await readCloudflareVersionInspection(options, configPath, versionId))?.metadata ?? null;
|
|
1632
1631
|
}
|
|
1633
|
-
async function readCloudflareVersionInspection(options,
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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 =
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
"--json"
|
|
1663
|
-
], { capture: true }));
|
|
1654
|
+
configPath,
|
|
1655
|
+
workerName: options.workerName
|
|
1656
|
+
}, { capture: true }));
|
|
1664
1657
|
if (isCleanCommandExit(inspection)) {
|
|
1665
|
-
const inspected =
|
|
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,
|
|
1680
|
-
const result = await Promise.resolve(options.runWrangler(
|
|
1681
|
-
"versions",
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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 (
|
|
1679
|
+
if (isCompletedCommand(result) && (result.failure === "not-found" || result.errorCode === 10007)) return [];
|
|
1690
1680
|
return null;
|
|
1691
1681
|
}
|
|
1692
1682
|
try {
|
|
1693
|
-
const versions =
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
options.
|
|
1778
|
-
|
|
1779
|
-
options.
|
|
1780
|
-
|
|
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 =
|
|
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(
|
|
1776
|
+
...isContainersUnavailable(bootstrap) ? { containersUnavailable: true } : {}
|
|
1797
1777
|
};
|
|
1798
1778
|
}
|
|
1799
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
1834
|
-
const
|
|
1835
|
-
|
|
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
|
|
1842
|
-
return
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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 (
|
|
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 = (
|
|
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,
|
|
1904
|
-
const owner = await readCurrentProductionVersion(options,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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 &&
|
|
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 = (
|
|
2108
|
-
return async (
|
|
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
|
-
|
|
2112
|
-
if (args[0] === "deployments" && args[1] === "status") {
|
|
2150
|
+
if (operation.type === "deployments.status") {
|
|
2113
2151
|
kind = "deployment";
|
|
2114
2152
|
resource = "/deployments";
|
|
2115
|
-
|
|
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
|
-
|
|
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 =
|
|
2123
|
-
rest = args.slice(3);
|
|
2158
|
+
resource = "/versions/" + encodeURIComponent(operation.versionId);
|
|
2124
2159
|
}
|
|
2125
|
-
if (!kind || !runOptions.capture ||
|
|
2126
|
-
if (requiresProxyTransport) return fallback(
|
|
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:
|
|
2187
|
+
stdout: "",
|
|
2188
|
+
data: latest,
|
|
2152
2189
|
stderr: ""
|
|
2153
2190
|
};
|
|
2154
2191
|
}
|
|
2155
|
-
if (
|
|
2192
|
+
if (operation.type === "versions.view" && result.id === operation.versionId) return {
|
|
2156
2193
|
status: 0,
|
|
2157
|
-
stdout:
|
|
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
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
+
"@types/node": "^26.6.3",
|
|
29
29
|
"pathslash": "^0.1.0",
|
|
30
30
|
"typescript": "7.0.2",
|
|
31
|
-
"vite-plus": "0.
|
|
31
|
+
"vite-plus": "1.0.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "vp pack",
|