devflare 1.0.0-next.20 → 1.0.0-next.21
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/LLM.md +9 -4
- package/README.md +10 -2
- package/dist/browser.js +3 -3
- package/dist/build-b1z6wqet.js +54 -0
- package/dist/build-x7maz3eb.js +54 -0
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/help-pages/pages/core.d.ts.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config-qj5jw8km.js +93 -0
- package/dist/deploy-jf3yczsz.js +1055 -0
- package/dist/deploy-xqm869nf.js +1055 -0
- package/dist/dev-bgpxrwms.js +2551 -0
- package/dist/dev-kzs65xcr.js +2551 -0
- package/dist/dev-server/dev-server-state.d.ts +2 -0
- package/dist/dev-server/dev-server-state.d.ts.map +1 -1
- package/dist/dev-server/miniflare-dev-config.d.ts +4 -0
- package/dist/dev-server/miniflare-dev-config.d.ts.map +1 -1
- package/dist/dev-server/server.d.ts.map +1 -1
- package/dist/dev-zgx7fhe9.js +2553 -0
- package/dist/doctor-0a2brpyz.js +259 -0
- package/dist/index-05pbj4hy.js +1193 -0
- package/dist/index-3edvz3hs.js +124 -0
- package/dist/index-50em8s6c.js +898 -0
- package/dist/index-666tdx14.js +895 -0
- package/dist/index-8p7rxkbs.js +1426 -0
- package/dist/index-aqrwyy57.js +288 -0
- package/dist/index-bj5avaba.js +109 -0
- package/dist/index-dgww0ewn.js +574 -0
- package/dist/index-f1yshy4s.js +412 -0
- package/dist/index-hpwa6vsw.js +239 -0
- package/dist/index-kxc4gtyt.js +574 -0
- package/dist/index-nxkesg55.js +68 -0
- package/dist/index-p7q23nce.js +1031 -0
- package/dist/index-pt49cgjv.js +1426 -0
- package/dist/index-rp0aye39.js +1426 -0
- package/dist/index-tknbyxzn.js +2202 -0
- package/dist/index.js +4 -4
- package/dist/runtime/index.js +4 -4
- package/dist/sveltekit/index.js +2 -2
- package/dist/test/index.js +62 -440
- package/dist/test/resolve-service-bindings.d.ts +63 -3
- package/dist/test/resolve-service-bindings.d.ts.map +1 -1
- package/dist/types-vhvt4hvm.js +693 -0
- package/dist/utils/send-email.d.ts.map +1 -1
- package/dist/utils/send-email.js +1 -1
- package/dist/vite/index.js +4 -3
- package/dist/vite/plugin-context.d.ts +3 -1
- package/dist/vite/plugin-context.d.ts.map +1 -1
- package/dist/vite/plugin-programmatic.d.ts.map +1 -1
- package/dist/vite/plugin-service-bindings.d.ts +13 -0
- package/dist/vite/plugin-service-bindings.d.ts.map +1 -0
- package/dist/vite/plugin.d.ts +4 -2
- package/dist/vite/plugin.d.ts.map +1 -1
- package/dist/worker-entrypoint-3rmzd4c1.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyDeploymentStrategy,
|
|
3
|
+
compareManifests,
|
|
4
|
+
createBuildManifest,
|
|
5
|
+
describeDeploymentStrategy,
|
|
6
|
+
formatDriftWarning,
|
|
7
|
+
prepareBuildArtifacts,
|
|
8
|
+
readBuildManifest
|
|
9
|
+
} from "./index-kxc4gtyt.js";
|
|
10
|
+
import"./index-aabgympv.js";
|
|
11
|
+
import {
|
|
12
|
+
preparePreviewScopedResourcesForDeploy
|
|
13
|
+
} from "./index-x12e6fzy.js";
|
|
14
|
+
import {
|
|
15
|
+
asOptionalString
|
|
16
|
+
} from "./index-53pqqpq9.js";
|
|
17
|
+
import {
|
|
18
|
+
formatWorkersDevUrl,
|
|
19
|
+
reconcilePreviewRegistry
|
|
20
|
+
} from "./index-3jme4hgw.js";
|
|
21
|
+
import {
|
|
22
|
+
getDependencies
|
|
23
|
+
} from "./index-z9gy8w6b.js";
|
|
24
|
+
import"./index-gn5wy09x.js";
|
|
25
|
+
import {
|
|
26
|
+
getPackageVersion
|
|
27
|
+
} from "./index-627srx16.js";
|
|
28
|
+
import {
|
|
29
|
+
createCliTheme,
|
|
30
|
+
dim,
|
|
31
|
+
green,
|
|
32
|
+
logLine,
|
|
33
|
+
yellow,
|
|
34
|
+
yellowBold
|
|
35
|
+
} from "./index-stgn34cr.js";
|
|
36
|
+
import"./index-3t6rypgc.js";
|
|
37
|
+
import"./index-gq39t0rx.js";
|
|
38
|
+
import {
|
|
39
|
+
resolvePackageSpecifier
|
|
40
|
+
} from "./index-t4fhcx1n.js";
|
|
41
|
+
import"./index-8x745h59.js";
|
|
42
|
+
import"./index-qf2dkqxh.js";
|
|
43
|
+
import"./index-qwgr4q7s.js";
|
|
44
|
+
import"./index-65e7xx1a.js";
|
|
45
|
+
import"./index-vhqww6tt.js";
|
|
46
|
+
import {
|
|
47
|
+
ServiceBindingValidationError,
|
|
48
|
+
compileBuildConfig,
|
|
49
|
+
compileConfig,
|
|
50
|
+
readWranglerConfig,
|
|
51
|
+
rebaseWranglerConfigPaths,
|
|
52
|
+
stringifyConfig,
|
|
53
|
+
validateServiceBindings,
|
|
54
|
+
writeWranglerConfig
|
|
55
|
+
} from "./index-c3nxftnp.js";
|
|
56
|
+
import {
|
|
57
|
+
loadConfig,
|
|
58
|
+
prepareConfigResourcesForDeploy,
|
|
59
|
+
resolveConfigForEnvironment
|
|
60
|
+
} from "./index-syscwrjp.js";
|
|
61
|
+
import {
|
|
62
|
+
getEffectiveAccountId,
|
|
63
|
+
getPrimaryAccount,
|
|
64
|
+
getWorkerVersionDetail,
|
|
65
|
+
getWorkersSubdomain,
|
|
66
|
+
listWorkerDeployments,
|
|
67
|
+
listWorkerVersions,
|
|
68
|
+
listWorkers
|
|
69
|
+
} from "./index-1d4jg11n.js";
|
|
70
|
+
import"./index-mg8vwqxf.js";
|
|
71
|
+
import {
|
|
72
|
+
resolvePreviewIdentifier
|
|
73
|
+
} from "./index-z40mjts9.js";
|
|
74
|
+
import"./index-q8f4kawk.js";
|
|
75
|
+
import {
|
|
76
|
+
__require
|
|
77
|
+
} from "./index-37x76zdn.js";
|
|
78
|
+
|
|
79
|
+
// src/cli/commands/deploy.ts
|
|
80
|
+
import { join } from "pathe";
|
|
81
|
+
|
|
82
|
+
// src/cli/deploy-target.ts
|
|
83
|
+
function resolveDeployTarget(parsed, options = {}) {
|
|
84
|
+
const wantsProduction = parsed.options.prod === true || parsed.options.production === true;
|
|
85
|
+
const previewOption = parsed.options.preview;
|
|
86
|
+
const previewScopeRaw = asOptionalString(previewOption);
|
|
87
|
+
const wantsPreview = previewOption === true || Boolean(previewScopeRaw);
|
|
88
|
+
if (!wantsProduction && !wantsPreview) {
|
|
89
|
+
if (options.requireExplicitTarget === true) {
|
|
90
|
+
throw new Error("Deploy needs an explicit target. Use --prod / --production for live traffic, or --preview <name> (or bare --preview) for preview deploys.");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
mode: "implicit",
|
|
94
|
+
envOverrides: {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (wantsProduction && wantsPreview) {
|
|
98
|
+
throw new Error("Choose either --prod / --production or --preview <name>, not both.");
|
|
99
|
+
}
|
|
100
|
+
const explicitEnvironment = asOptionalString(parsed.options.env);
|
|
101
|
+
if (wantsProduction) {
|
|
102
|
+
if (explicitEnvironment && explicitEnvironment !== "production") {
|
|
103
|
+
throw new Error("Production deploys always target the production environment. Remove --env or use --env production.");
|
|
104
|
+
}
|
|
105
|
+
if (parsed.options["branch-name"] !== undefined) {
|
|
106
|
+
throw new Error("Production deploys do not accept --branch-name.");
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
mode: "production",
|
|
110
|
+
environment: "production",
|
|
111
|
+
targetFlag: parsed.options.production === true ? "--production" : "--prod",
|
|
112
|
+
envOverrides: {
|
|
113
|
+
DEVFLARE_PREVIEW_BRANCH: undefined,
|
|
114
|
+
DEVFLARE_PREVIEW_IDENTIFIER: undefined,
|
|
115
|
+
DEVFLARE_PREVIEW_PR: undefined
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (explicitEnvironment && explicitEnvironment !== "preview") {
|
|
120
|
+
throw new Error("Preview deploys always target the preview environment. Remove --env or use --env preview.");
|
|
121
|
+
}
|
|
122
|
+
if (previewScopeRaw) {
|
|
123
|
+
const branchName = asOptionalString(parsed.options["branch-name"]);
|
|
124
|
+
if (branchName && branchName !== previewScopeRaw) {
|
|
125
|
+
throw new Error("Named preview deploys use the --preview value as the preview scope. Omit --branch-name or pass the same value to both flags.");
|
|
126
|
+
}
|
|
127
|
+
const previewScope = resolvePreviewIdentifier({
|
|
128
|
+
identifier: previewScopeRaw
|
|
129
|
+
}).identifier ?? "preview";
|
|
130
|
+
return {
|
|
131
|
+
mode: "preview-scope",
|
|
132
|
+
environment: "preview",
|
|
133
|
+
targetFlag: "--preview",
|
|
134
|
+
previewScope,
|
|
135
|
+
previewScopeRaw,
|
|
136
|
+
envOverrides: {
|
|
137
|
+
DEVFLARE_PREVIEW_BRANCH: previewScopeRaw,
|
|
138
|
+
DEVFLARE_PREVIEW_IDENTIFIER: previewScope,
|
|
139
|
+
DEVFLARE_PREVIEW_PR: undefined
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
mode: "preview-upload",
|
|
145
|
+
environment: "preview",
|
|
146
|
+
targetFlag: "--preview",
|
|
147
|
+
envOverrides: {
|
|
148
|
+
DEVFLARE_PREVIEW_BRANCH: undefined,
|
|
149
|
+
DEVFLARE_PREVIEW_IDENTIFIER: undefined,
|
|
150
|
+
DEVFLARE_PREVIEW_PR: undefined
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function applyResolvedDeployTarget(parsed, target) {
|
|
155
|
+
if (!target.environment) {
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
...parsed,
|
|
160
|
+
options: {
|
|
161
|
+
...parsed.options,
|
|
162
|
+
env: target.environment
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function withTemporaryEnvironment(overrides, operation) {
|
|
167
|
+
const previousValues = new Map;
|
|
168
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
169
|
+
previousValues.set(key, process.env[key]);
|
|
170
|
+
if (typeof value === "string") {
|
|
171
|
+
process.env[key] = value;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
delete process.env[key];
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return await operation();
|
|
178
|
+
} finally {
|
|
179
|
+
for (const [key, value] of previousValues) {
|
|
180
|
+
if (typeof value === "string") {
|
|
181
|
+
process.env[key] = value;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
delete process.env[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/cli/preview.ts
|
|
190
|
+
function matchNamedValue(output, patterns) {
|
|
191
|
+
for (const pattern of patterns) {
|
|
192
|
+
const match = output.match(pattern);
|
|
193
|
+
if (match?.[1]) {
|
|
194
|
+
return match[1];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
function appendUniqueUrls(target, value) {
|
|
200
|
+
if (typeof value === "string") {
|
|
201
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
202
|
+
target.push(value);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const item of value) {
|
|
210
|
+
appendUniqueUrls(target, item);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function parseWranglerStructuredOutput(output) {
|
|
214
|
+
const records = output.replace(/\r/g, "").split(`
|
|
215
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(line);
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}).filter((record) => record !== null);
|
|
222
|
+
const urls = [];
|
|
223
|
+
let versionId;
|
|
224
|
+
let previewUrl;
|
|
225
|
+
for (const record of records) {
|
|
226
|
+
if (!versionId && typeof record.version_id === "string" && record.version_id.trim()) {
|
|
227
|
+
versionId = record.version_id.trim();
|
|
228
|
+
}
|
|
229
|
+
appendUniqueUrls(urls, record.targets);
|
|
230
|
+
appendUniqueUrls(urls, record.preview_urls);
|
|
231
|
+
appendUniqueUrls(urls, record.urls);
|
|
232
|
+
appendUniqueUrls(urls, record.url);
|
|
233
|
+
if (!previewUrl && typeof record.preview_url === "string" && record.preview_url.trim()) {
|
|
234
|
+
previewUrl = record.preview_url.trim();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const uniqueUrls = [...new Set(urls)];
|
|
238
|
+
if (!previewUrl) {
|
|
239
|
+
previewUrl = uniqueUrls.find((url) => url.includes("workers.dev"));
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
versionId,
|
|
243
|
+
previewUrl,
|
|
244
|
+
urls: uniqueUrls
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function mergeParsedWranglerDeployOutputs(...outputs) {
|
|
248
|
+
const urls = [...new Set(outputs.flatMap((output) => output.urls))];
|
|
249
|
+
return {
|
|
250
|
+
versionId: outputs.map((output) => output.versionId).find((value) => Boolean(value)),
|
|
251
|
+
previewUrl: outputs.map((output) => output.previewUrl).find((value) => Boolean(value)) ?? urls.find((url) => url.includes("workers.dev")),
|
|
252
|
+
urls
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function parseWranglerDeployOutput(output) {
|
|
256
|
+
const normalizedOutput = output.replace(/\r/g, "");
|
|
257
|
+
const urls = [...new Set(normalizedOutput.match(/https?:\/\/[^\s'"`]+/g) ?? [])];
|
|
258
|
+
const previewUrl = matchNamedValue(normalizedOutput, [
|
|
259
|
+
/Preview URL:\s*(https?:\/\/\S+)/i,
|
|
260
|
+
/Version Preview URL:\s*(https?:\/\/\S+)/i
|
|
261
|
+
]) ?? urls.find((url) => url.includes("workers.dev"));
|
|
262
|
+
const versionId = matchNamedValue(normalizedOutput, [
|
|
263
|
+
/Worker Version ID:\s*([A-Za-z0-9_-]+)/i,
|
|
264
|
+
/Version ID:\s*([A-Za-z0-9_-]+)/i,
|
|
265
|
+
/version(?:_id| id)?\s*[:=]\s*([A-Za-z0-9_-]+)/i,
|
|
266
|
+
/"id"\s*:\s*"([A-Za-z0-9_-]+)"/i
|
|
267
|
+
]);
|
|
268
|
+
return {
|
|
269
|
+
versionId,
|
|
270
|
+
previewUrl,
|
|
271
|
+
urls
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/cli/commands/deploy/metadata.ts
|
|
276
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
277
|
+
import { dirname } from "pathe";
|
|
278
|
+
function inferRecordSource() {
|
|
279
|
+
return process.env.GITHUB_ACTIONS === "true" ? "github-action" : "cli";
|
|
280
|
+
}
|
|
281
|
+
async function writeDeployResultMetadata(metadata) {
|
|
282
|
+
const metadataPath = process.env.DEVFLARE_DEPLOY_METADATA_PATH?.trim();
|
|
283
|
+
if (!metadataPath) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
287
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, "\t"), "utf8");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/cli/commands/deploy/prepare.ts
|
|
291
|
+
import { mkdir as mkdir2, open, readFile, rm } from "node:fs/promises";
|
|
292
|
+
import { basename, dirname as dirname2, isAbsolute, resolve } from "pathe";
|
|
293
|
+
function summarizeDeployResourceNames(resources) {
|
|
294
|
+
const segments = [
|
|
295
|
+
resources.kv.length > 0 ? `KV ${resources.kv.length}` : null,
|
|
296
|
+
resources.d1.length > 0 ? `D1 ${resources.d1.length}` : null,
|
|
297
|
+
resources.r2.length > 0 ? `R2 ${resources.r2.length}` : null,
|
|
298
|
+
resources.queues.length > 0 ? `Queues ${resources.queues.length}` : null,
|
|
299
|
+
resources.vectorize.length > 0 ? `Vectorize ${resources.vectorize.length}` : null,
|
|
300
|
+
resources.hyperdrive.length > 0 ? `Hyperdrive ${resources.hyperdrive.length}` : null
|
|
301
|
+
].filter((segment) => segment !== null);
|
|
302
|
+
return segments.length > 0 ? segments.join(" · ") : null;
|
|
303
|
+
}
|
|
304
|
+
async function readDeployRedirectPath(filePath) {
|
|
305
|
+
const fs = await import("node:fs/promises");
|
|
306
|
+
try {
|
|
307
|
+
const rawConfig = await fs.readFile(filePath, "utf-8");
|
|
308
|
+
const parsed = JSON.parse(rawConfig);
|
|
309
|
+
if (typeof parsed.configPath !== "string" || parsed.configPath.length === 0) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return resolve(dirname2(filePath), parsed.configPath);
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function resolveBuildArtifactConfigPath(buildPath, cwd) {
|
|
318
|
+
const fs = await import("node:fs/promises");
|
|
319
|
+
const absoluteBuildPath = isAbsolute(buildPath) ? buildPath : resolve(cwd, buildPath);
|
|
320
|
+
let stat;
|
|
321
|
+
try {
|
|
322
|
+
stat = await fs.stat(absoluteBuildPath);
|
|
323
|
+
} catch {
|
|
324
|
+
throw new Error(`Could not find build artifact path: ${absoluteBuildPath}`);
|
|
325
|
+
}
|
|
326
|
+
if (stat.isFile()) {
|
|
327
|
+
if (basename(absoluteBuildPath) === "config.json") {
|
|
328
|
+
const redirectedConfigPath = await readDeployRedirectPath(absoluteBuildPath);
|
|
329
|
+
if (!redirectedConfigPath) {
|
|
330
|
+
throw new Error(`Build redirect ${absoluteBuildPath} did not contain a valid configPath.`);
|
|
331
|
+
}
|
|
332
|
+
return redirectedConfigPath;
|
|
333
|
+
}
|
|
334
|
+
return absoluteBuildPath;
|
|
335
|
+
}
|
|
336
|
+
const candidates = [
|
|
337
|
+
resolve(absoluteBuildPath, "wrangler.jsonc"),
|
|
338
|
+
resolve(absoluteBuildPath, ".wrangler", "deploy", "config.json"),
|
|
339
|
+
resolve(absoluteBuildPath, "config.json"),
|
|
340
|
+
resolve(absoluteBuildPath, ".devflare", "build", "wrangler.jsonc")
|
|
341
|
+
];
|
|
342
|
+
for (const candidatePath of candidates) {
|
|
343
|
+
try {
|
|
344
|
+
const candidateStat = await fs.stat(candidatePath);
|
|
345
|
+
if (!candidateStat.isFile()) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (basename(candidatePath) === "config.json") {
|
|
349
|
+
const redirectedConfigPath = await readDeployRedirectPath(candidatePath);
|
|
350
|
+
if (redirectedConfigPath) {
|
|
351
|
+
return redirectedConfigPath;
|
|
352
|
+
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
return candidatePath;
|
|
356
|
+
} catch {}
|
|
357
|
+
}
|
|
358
|
+
throw new Error(`Could not resolve a Wrangler build config from ${absoluteBuildPath}. Pass a .devflare/build directory, a generated wrangler.jsonc, or a .wrangler/deploy/config.json redirect.`);
|
|
359
|
+
}
|
|
360
|
+
function withBuildArtifactPaths(compiledConfig, buildConfig) {
|
|
361
|
+
return {
|
|
362
|
+
...compiledConfig,
|
|
363
|
+
...buildConfig.main ? { main: buildConfig.main } : {},
|
|
364
|
+
...buildConfig.assets ? { assets: buildConfig.assets } : {}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function prepareDeployConfig(options) {
|
|
368
|
+
const rawConfig = await loadConfig({
|
|
369
|
+
cwd: options.cwd,
|
|
370
|
+
configFile: options.configPath
|
|
371
|
+
});
|
|
372
|
+
const manifestDir = dirname2(options.buildConfigPath);
|
|
373
|
+
const manifest = await readBuildManifest(manifestDir);
|
|
374
|
+
if (manifest) {
|
|
375
|
+
const currentManifest = createBuildManifest(rawConfig, {
|
|
376
|
+
devflareVersion: await getPackageVersion(),
|
|
377
|
+
intendedTarget: {
|
|
378
|
+
environment: options.environment,
|
|
379
|
+
preview: options.preview,
|
|
380
|
+
branchName: options.branchName
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
const drift = compareManifests(manifest, currentManifest);
|
|
384
|
+
const warning = formatDriftWarning(drift);
|
|
385
|
+
if (warning && options.logger) {
|
|
386
|
+
if (options.force) {
|
|
387
|
+
logLine(options.logger, warning);
|
|
388
|
+
logLine(options.logger, "Continuing because --force was passed.");
|
|
389
|
+
} else {
|
|
390
|
+
logLine(options.logger, warning);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const previewScopedResources = options.environment === "preview" ? await preparePreviewScopedResourcesForDeploy(rawConfig, {
|
|
395
|
+
environment: options.environment
|
|
396
|
+
}) : null;
|
|
397
|
+
const deployResources = await prepareConfigResourcesForDeploy(previewScopedResources?.config ?? rawConfig, {
|
|
398
|
+
environment: options.environment,
|
|
399
|
+
accountId: previewScopedResources?.accountId,
|
|
400
|
+
cloudflare: previewScopedResources?.resourceResolutionCloudflare
|
|
401
|
+
});
|
|
402
|
+
const deploymentStrategy = applyDeploymentStrategy(deployResources.config, {
|
|
403
|
+
environment: options.environment,
|
|
404
|
+
preview: options.preview,
|
|
405
|
+
branchName: options.branchName,
|
|
406
|
+
previewBranch: process.env.DEVFLARE_PREVIEW_BRANCH
|
|
407
|
+
});
|
|
408
|
+
const validationAccountId = previewScopedResources?.accountId ?? deploymentStrategy.config.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
409
|
+
if (validationAccountId) {
|
|
410
|
+
try {
|
|
411
|
+
await validateServiceBindings(deploymentStrategy.config, validationAccountId, {
|
|
412
|
+
listWorkers: (accountId) => listWorkers(accountId),
|
|
413
|
+
selfWorkerName: deploymentStrategy.config.name
|
|
414
|
+
});
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error instanceof ServiceBindingValidationError) {
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const buildWranglerConfig = await readWranglerConfig(options.buildConfigPath);
|
|
422
|
+
const compiledWranglerConfig = compileConfig(deploymentStrategy.config);
|
|
423
|
+
const buildDir = dirname2(options.buildConfigPath);
|
|
424
|
+
const deployArtefactDir = resolve(buildDir, "..", "deploy");
|
|
425
|
+
await mkdir2(deployArtefactDir, { recursive: true });
|
|
426
|
+
const deployArtefactPath = resolve(deployArtefactDir, "wrangler.jsonc");
|
|
427
|
+
const compiledRebased = rebaseWranglerConfigPaths(options.cwd, deployArtefactDir, compiledWranglerConfig);
|
|
428
|
+
const buildRebased = rebaseWranglerConfigPaths(buildDir, deployArtefactDir, buildWranglerConfig);
|
|
429
|
+
const wranglerConfig = withBuildArtifactPaths(compiledRebased, buildRebased);
|
|
430
|
+
const lockPath = resolve(deployArtefactDir, ".lock");
|
|
431
|
+
const lockHandle = await acquireDeployArtefactLock(lockPath);
|
|
432
|
+
try {
|
|
433
|
+
await writeWranglerConfig(deployArtefactDir, wranglerConfig, "wrangler.jsonc");
|
|
434
|
+
} finally {
|
|
435
|
+
await releaseDeployArtefactLock(lockHandle, lockPath);
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
config: deploymentStrategy.config,
|
|
439
|
+
deployConfigPath: deployArtefactPath,
|
|
440
|
+
previewScopedResources,
|
|
441
|
+
deployResources,
|
|
442
|
+
wranglerConfig
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function acquireDeployArtefactLock(lockPath, options = {}) {
|
|
446
|
+
const maxWaitMs = options.maxWaitMs ?? 30000;
|
|
447
|
+
const staleAfterMs = options.staleAfterMs ?? 60000;
|
|
448
|
+
const pollMs = 100;
|
|
449
|
+
const start = Date.now();
|
|
450
|
+
while (true) {
|
|
451
|
+
try {
|
|
452
|
+
const handle = await open(lockPath, "wx");
|
|
453
|
+
await handle.writeFile(`${process.pid}
|
|
454
|
+
${Date.now()}`);
|
|
455
|
+
return handle;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err.code !== "EEXIST")
|
|
458
|
+
throw err;
|
|
459
|
+
try {
|
|
460
|
+
const existing = await readFile(lockPath, "utf-8");
|
|
461
|
+
const ts = Number.parseInt(existing.split(`
|
|
462
|
+
`)[1] ?? "0", 10);
|
|
463
|
+
if (Number.isFinite(ts) && Date.now() - ts > staleAfterMs) {
|
|
464
|
+
await rm(lockPath, { force: true });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (Date.now() - start > maxWaitMs) {
|
|
471
|
+
throw new Error(`Timed out waiting for deploy artefact lock at ${lockPath}. Another \`devflare deploy\` may be running against the same artefact directory.`);
|
|
472
|
+
}
|
|
473
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function releaseDeployArtefactLock(handle, lockPath) {
|
|
478
|
+
try {
|
|
479
|
+
await handle.close();
|
|
480
|
+
} catch {}
|
|
481
|
+
await rm(lockPath, { force: true });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/cli/commands/deploy/runtime.ts
|
|
485
|
+
async function resolveLocalWranglerExecutable(cwd, fs) {
|
|
486
|
+
const wranglerExecutablePath = resolvePackageSpecifier("wrangler/bin/wrangler.js", cwd);
|
|
487
|
+
try {
|
|
488
|
+
await fs.access(wranglerExecutablePath);
|
|
489
|
+
return wranglerExecutablePath;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/cli/commands/deploy/verification.ts
|
|
496
|
+
function shouldVerifyDeployControlPlane() {
|
|
497
|
+
const configured = process.env.DEVFLARE_VERIFY_DEPLOYMENT?.trim().toLowerCase();
|
|
498
|
+
if (!configured) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return !["0", "false", "no", "off"].includes(configured);
|
|
502
|
+
}
|
|
503
|
+
function shouldRequireFreshProductionDeployment() {
|
|
504
|
+
const configured = process.env.DEVFLARE_REQUIRE_FRESH_PRODUCTION_DEPLOYMENT?.trim().toLowerCase();
|
|
505
|
+
if (!configured) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
return !["0", "false", "no", "off"].includes(configured);
|
|
509
|
+
}
|
|
510
|
+
function getDeployVerificationSettings() {
|
|
511
|
+
const attempts = Number.parseInt(process.env.DEVFLARE_VERIFY_DEPLOYMENT_ATTEMPTS ?? "", 10);
|
|
512
|
+
const delayMs = Number.parseInt(process.env.DEVFLARE_VERIFY_DEPLOYMENT_DELAY_MS ?? "", 10);
|
|
513
|
+
return {
|
|
514
|
+
attempts: Number.isFinite(attempts) && attempts > 0 ? attempts : 5,
|
|
515
|
+
delayMs: Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 1500
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
var DEPLOYMENT_LOOKBACK_TOLERANCE_MS = 2 * 60 * 1000;
|
|
519
|
+
function normalizeCloudflareAccountId(value) {
|
|
520
|
+
const trimmed = value?.trim();
|
|
521
|
+
if (!trimmed) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
return /^[a-f0-9]{32}$/i.test(trimmed) ? trimmed : undefined;
|
|
525
|
+
}
|
|
526
|
+
async function waitForDeployVerification(delayMs) {
|
|
527
|
+
if (delayMs <= 0) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
531
|
+
}
|
|
532
|
+
async function retryDeployVerification(description, operation) {
|
|
533
|
+
const { attempts, delayMs } = getDeployVerificationSettings();
|
|
534
|
+
let lastError;
|
|
535
|
+
for (let attempt = 1;attempt <= attempts; attempt++) {
|
|
536
|
+
try {
|
|
537
|
+
return await operation();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
lastError = error;
|
|
540
|
+
if (attempt < attempts) {
|
|
541
|
+
await waitForDeployVerification(delayMs);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
546
|
+
throw new Error(`Cloudflare could not verify ${description} after ${attempts} attempt${attempts === 1 ? "" : "s"}: ${message}`);
|
|
547
|
+
}
|
|
548
|
+
async function resolveDeployAccountId(preferredAccountId) {
|
|
549
|
+
if (preferredAccountId !== undefined) {
|
|
550
|
+
return normalizeCloudflareAccountId(preferredAccountId);
|
|
551
|
+
}
|
|
552
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN?.trim();
|
|
553
|
+
const apiKey = process.env.CLOUDFLARE_API_KEY?.trim();
|
|
554
|
+
const apiEmail = process.env.CLOUDFLARE_EMAIL?.trim();
|
|
555
|
+
if (!apiToken && !(apiKey && apiEmail)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const primaryAccount = await getPrimaryAccount();
|
|
560
|
+
if (!primaryAccount) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const effective = await getEffectiveAccountId(primaryAccount.id);
|
|
564
|
+
return normalizeCloudflareAccountId(effective.accountId);
|
|
565
|
+
} catch {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function selectDeploymentVersionId(deployment) {
|
|
570
|
+
return deployment.versions.find((version) => version.percentage === 100)?.versionId ?? deployment.versions[0]?.versionId;
|
|
571
|
+
}
|
|
572
|
+
function getWorkerVersionTimestamp(version) {
|
|
573
|
+
return version.metadata.modifiedOn ?? version.metadata.createdOn;
|
|
574
|
+
}
|
|
575
|
+
async function resolveVersionIdFromLatestDeployment(options) {
|
|
576
|
+
return retryDeployVerification(options.verificationDescription, async () => {
|
|
577
|
+
const deployments = await listWorkerDeployments(options.accountId, options.workerName);
|
|
578
|
+
const latestDeployment = [...deployments].sort((a, b) => b.createdOn.getTime() - a.createdOn.getTime())[0];
|
|
579
|
+
if (!latestDeployment) {
|
|
580
|
+
throw new Error(`No deployments were found for Worker "${options.workerName}".`);
|
|
581
|
+
}
|
|
582
|
+
if (options.deployedAfter && latestDeployment.createdOn.getTime() < options.deployedAfter.getTime() - DEPLOYMENT_LOOKBACK_TOLERANCE_MS) {
|
|
583
|
+
throw new Error(`${options.deploymentLabel} ${latestDeployment.id} was created before this deploy started.`);
|
|
584
|
+
}
|
|
585
|
+
const versionId = selectDeploymentVersionId(latestDeployment);
|
|
586
|
+
if (!versionId) {
|
|
587
|
+
throw new Error(`${options.deploymentLabel} ${latestDeployment.id} does not reference any version ids.`);
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
deploymentId: latestDeployment.id,
|
|
591
|
+
versionId
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
async function resolveVersionIdFromLatestWorkerVersion(options) {
|
|
596
|
+
return retryDeployVerification(`the latest ${options.preview ? "preview " : ""}version for Worker "${options.workerName}"`, async () => {
|
|
597
|
+
const versions = await listWorkerVersions(options.accountId, options.workerName);
|
|
598
|
+
const latestVersion = [...versions].filter((version) => version.id).filter((version) => version.metadata.hasPreview === options.preview).sort((a, b) => {
|
|
599
|
+
const left = getWorkerVersionTimestamp(a)?.getTime() ?? 0;
|
|
600
|
+
const right = getWorkerVersionTimestamp(b)?.getTime() ?? 0;
|
|
601
|
+
return right - left;
|
|
602
|
+
})[0];
|
|
603
|
+
if (!latestVersion) {
|
|
604
|
+
throw new Error(`No ${options.preview ? "preview " : ""}versions were found for Worker "${options.workerName}".`);
|
|
605
|
+
}
|
|
606
|
+
const latestVersionTimestamp = getWorkerVersionTimestamp(latestVersion);
|
|
607
|
+
if (!latestVersionTimestamp) {
|
|
608
|
+
throw new Error(`Latest version ${latestVersion.id} did not include a creation timestamp.`);
|
|
609
|
+
}
|
|
610
|
+
if (latestVersionTimestamp.getTime() < options.deployedAfter.getTime() - DEPLOYMENT_LOOKBACK_TOLERANCE_MS) {
|
|
611
|
+
throw new Error(`Latest version ${latestVersion.id} was created before this deploy started.`);
|
|
612
|
+
}
|
|
613
|
+
return latestVersion.id;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async function resolveVersionIdFromLatestProductionDeployment(options) {
|
|
617
|
+
return resolveVersionIdFromLatestDeployment({
|
|
618
|
+
accountId: options.accountId,
|
|
619
|
+
workerName: options.workerName,
|
|
620
|
+
verificationDescription: `the latest deployment for Worker "${options.workerName}"`,
|
|
621
|
+
deploymentLabel: "Latest deployment",
|
|
622
|
+
deployedAfter: options.deployedAfter
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async function resolveVersionIdFromCurrentProductionDeployment(options) {
|
|
626
|
+
return resolveVersionIdFromLatestDeployment({
|
|
627
|
+
accountId: options.accountId,
|
|
628
|
+
workerName: options.workerName,
|
|
629
|
+
verificationDescription: `the current active deployment for Worker "${options.workerName}"`,
|
|
630
|
+
deploymentLabel: "Current deployment"
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function verifyDeployControlPlane(options) {
|
|
634
|
+
logLine(options.logger, dim("Verifying Cloudflare control-plane state…", options.theme));
|
|
635
|
+
await retryDeployVerification(`Worker version ${options.versionId}`, async () => {
|
|
636
|
+
const version = await getWorkerVersionDetail(options.accountId, options.workerName, options.versionId);
|
|
637
|
+
if (!version.id) {
|
|
638
|
+
throw new Error(`Cloudflare returned an empty version record for ${options.versionId}.`);
|
|
639
|
+
}
|
|
640
|
+
return version;
|
|
641
|
+
});
|
|
642
|
+
if (options.preview) {
|
|
643
|
+
options.logger.success(`Verified preview upload in Cloudflare control plane for version ${options.versionId}`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const deployment = await retryDeployVerification(`a deployment that references version ${options.versionId}`, async () => {
|
|
647
|
+
const deployments = await listWorkerDeployments(options.accountId, options.workerName);
|
|
648
|
+
const match = deployments.find((item) => item.versions.some((version) => version.versionId === options.versionId));
|
|
649
|
+
if (!match) {
|
|
650
|
+
throw new Error(`No deployment for Worker "${options.workerName}" references version ${options.versionId} yet.`);
|
|
651
|
+
}
|
|
652
|
+
return match;
|
|
653
|
+
});
|
|
654
|
+
options.logger.success(`Verified Cloudflare deployment ${deployment.id} for version ${options.versionId}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/cli/commands/deploy.ts
|
|
658
|
+
async function runDeployCommand(parsed, logger, options) {
|
|
659
|
+
let deployTarget = {
|
|
660
|
+
mode: "implicit",
|
|
661
|
+
envOverrides: {}
|
|
662
|
+
};
|
|
663
|
+
let resolvedParsed = parsed;
|
|
664
|
+
const cwd = options.cwd || process.cwd();
|
|
665
|
+
let configPath;
|
|
666
|
+
let environment;
|
|
667
|
+
let dryRun = false;
|
|
668
|
+
let preview = false;
|
|
669
|
+
let branchName;
|
|
670
|
+
let deployMessage;
|
|
671
|
+
let deployTag;
|
|
672
|
+
let previewScopeName;
|
|
673
|
+
let requireFreshProductionDeployment = false;
|
|
674
|
+
let resolvedPreviewScopeName = process.env.DEVFLARE_PREVIEW_BRANCH?.trim() || undefined;
|
|
675
|
+
const theme = createCliTheme(parsed.options);
|
|
676
|
+
logLine(logger);
|
|
677
|
+
logLine(logger, `${yellowBold("deploy", theme)} ${dim("Shipping to Cloudflare", theme)}`);
|
|
678
|
+
try {
|
|
679
|
+
deployTarget = resolveDeployTarget(parsed, {
|
|
680
|
+
requireExplicitTarget: options.requireExplicitDeployTarget === true
|
|
681
|
+
});
|
|
682
|
+
resolvedParsed = applyResolvedDeployTarget(parsed, deployTarget);
|
|
683
|
+
configPath = resolvedParsed.options.config;
|
|
684
|
+
environment = resolvedParsed.options.env;
|
|
685
|
+
dryRun = resolvedParsed.options["dry-run"] === true;
|
|
686
|
+
preview = deployTarget.mode === "preview-upload";
|
|
687
|
+
branchName = resolvedParsed.options["branch-name"];
|
|
688
|
+
deployMessage = resolvedParsed.options.message;
|
|
689
|
+
deployTag = resolvedParsed.options.tag;
|
|
690
|
+
previewScopeName = branchName?.trim() || deployTarget.previewScopeRaw || undefined;
|
|
691
|
+
resolvedPreviewScopeName = previewScopeName || process.env.DEVFLARE_PREVIEW_BRANCH?.trim() || undefined;
|
|
692
|
+
requireFreshProductionDeployment = !preview && shouldRequireFreshProductionDeployment();
|
|
693
|
+
return await withTemporaryEnvironment(deployTarget.envOverrides, async () => {
|
|
694
|
+
resolvedPreviewScopeName = previewScopeName || process.env.DEVFLARE_PREVIEW_BRANCH?.trim() || undefined;
|
|
695
|
+
if (dryRun) {
|
|
696
|
+
const config = await loadConfig({ cwd, configFile: configPath });
|
|
697
|
+
const deploymentStrategy = applyDeploymentStrategy(resolveConfigForEnvironment(config, environment), {
|
|
698
|
+
environment,
|
|
699
|
+
preview,
|
|
700
|
+
branchName,
|
|
701
|
+
previewBranch: process.env.DEVFLARE_PREVIEW_BRANCH
|
|
702
|
+
});
|
|
703
|
+
const wranglerConfig = compileBuildConfig(deploymentStrategy.config, undefined, {
|
|
704
|
+
alreadyResolved: true
|
|
705
|
+
});
|
|
706
|
+
logLine(logger, `${yellow("dry run", theme)} ${dim("Skipping actual deployment", theme)}`);
|
|
707
|
+
const deploymentStrategyMessage = describeDeploymentStrategy(deploymentStrategy);
|
|
708
|
+
if (deploymentStrategyMessage) {
|
|
709
|
+
logLine(logger, dim(deploymentStrategyMessage, theme));
|
|
710
|
+
}
|
|
711
|
+
logLine(logger, dim("Would deploy with wrangler config:", theme));
|
|
712
|
+
logLine(logger, stringifyConfig(wranglerConfig));
|
|
713
|
+
try {
|
|
714
|
+
const describeResult = await prepareConfigResourcesForDeploy(deploymentStrategy.config, {
|
|
715
|
+
environment,
|
|
716
|
+
describeOnly: true
|
|
717
|
+
});
|
|
718
|
+
const resolvedWranglerConfig = compileConfig(describeResult.config);
|
|
719
|
+
logLine(logger, dim("Resolved view (would-create placeholders for missing resources):", theme));
|
|
720
|
+
logLine(logger, stringifyConfig(resolvedWranglerConfig));
|
|
721
|
+
const wouldCreate = [
|
|
722
|
+
...describeResult.created.kv.map((n) => `KV: ${n}`),
|
|
723
|
+
...describeResult.created.d1.map((n) => `D1: ${n}`),
|
|
724
|
+
...describeResult.created.r2.map((n) => `R2: ${n}`),
|
|
725
|
+
...describeResult.created.queues.map((n) => `Queue: ${n}`)
|
|
726
|
+
];
|
|
727
|
+
if (wouldCreate.length > 0) {
|
|
728
|
+
logLine(logger, dim(`Would create:
|
|
729
|
+
- ${wouldCreate.join(`
|
|
730
|
+
- `)}`, theme));
|
|
731
|
+
}
|
|
732
|
+
} catch (describeErr) {
|
|
733
|
+
logLine(logger, dim(`(resolved view unavailable: ${describeErr.message})`, theme));
|
|
734
|
+
}
|
|
735
|
+
return { exitCode: 0 };
|
|
736
|
+
}
|
|
737
|
+
const deps = await getDependencies();
|
|
738
|
+
const requestedBuildPath = resolvedParsed.options.build;
|
|
739
|
+
const buildConfigPath = requestedBuildPath ? await resolveBuildArtifactConfigPath(requestedBuildPath, cwd) : (await prepareBuildArtifacts(resolvedParsed, logger, options)).deployConfigPath;
|
|
740
|
+
const prepared = await prepareDeployConfig({
|
|
741
|
+
cwd,
|
|
742
|
+
configPath,
|
|
743
|
+
environment,
|
|
744
|
+
buildConfigPath,
|
|
745
|
+
preview,
|
|
746
|
+
branchName,
|
|
747
|
+
logger,
|
|
748
|
+
force: resolvedParsed.options.force === true
|
|
749
|
+
});
|
|
750
|
+
const createdPreviewResourcesSummary = prepared.previewScopedResources ? summarizeDeployResourceNames(prepared.previewScopedResources.created) : null;
|
|
751
|
+
if (createdPreviewResourcesSummary) {
|
|
752
|
+
logLine(logger, `Provisioned preview-scoped resources: ${createdPreviewResourcesSummary}`);
|
|
753
|
+
}
|
|
754
|
+
const existingPreviewResourcesSummary = prepared.previewScopedResources ? summarizeDeployResourceNames(prepared.previewScopedResources.existing) : null;
|
|
755
|
+
if (existingPreviewResourcesSummary) {
|
|
756
|
+
logLine(logger, `Reused preview-scoped resources: ${existingPreviewResourcesSummary}`);
|
|
757
|
+
}
|
|
758
|
+
const createdDeployResourcesSummary = summarizeDeployResourceNames(prepared.deployResources.created);
|
|
759
|
+
if (createdDeployResourcesSummary) {
|
|
760
|
+
logLine(logger, `Provisioned deploy resources: ${createdDeployResourcesSummary}`);
|
|
761
|
+
}
|
|
762
|
+
const existingDeployResourcesSummary = summarizeDeployResourceNames(prepared.deployResources.existing);
|
|
763
|
+
if (existingDeployResourcesSummary) {
|
|
764
|
+
logLine(logger, `Reused deploy resources: ${existingDeployResourcesSummary}`);
|
|
765
|
+
}
|
|
766
|
+
for (const warning of prepared.previewScopedResources?.warnings ?? []) {
|
|
767
|
+
logger.warn(warning);
|
|
768
|
+
}
|
|
769
|
+
for (const warning of prepared.deployResources.warnings) {
|
|
770
|
+
logger.warn(warning);
|
|
771
|
+
}
|
|
772
|
+
if (requestedBuildPath) {
|
|
773
|
+
logLine(logger, `${dim("build", theme)} ${green(buildConfigPath, theme)}`);
|
|
774
|
+
}
|
|
775
|
+
logLine(logger, `${dim("worker", theme)} ${green(prepared.config.name, theme)}`);
|
|
776
|
+
const localWranglerExecutable = await resolveLocalWranglerExecutable(cwd, deps.fs);
|
|
777
|
+
const isBranchScopedPreviewDeployment = !preview && environment === "preview" && typeof resolvedPreviewScopeName === "string" && resolvedPreviewScopeName.length > 0;
|
|
778
|
+
if (preview) {
|
|
779
|
+
logger.warn("Cloudflare preview uploads cannot be the first upload for a brand-new Worker.");
|
|
780
|
+
if (prepared.config.bindings?.durableObjects && Object.keys(prepared.config.bindings.durableObjects).length > 0) {
|
|
781
|
+
logger.warn("Cloudflare does not currently generate preview URLs for Workers that implement Durable Objects.");
|
|
782
|
+
}
|
|
783
|
+
if (prepared.config.migrations && prepared.config.migrations.length > 0) {
|
|
784
|
+
logger.warn("Cloudflare versions upload does not currently support Durable Object migrations.");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
logLine(logger, dim(preview ? "Uploading preview version with Wrangler…" : "Deploying with Wrangler…", theme));
|
|
788
|
+
const deployStartedAt = new Date;
|
|
789
|
+
const wranglerOutputDirectory = join(cwd, ".devflare");
|
|
790
|
+
const wranglerOutputFilePath = join(wranglerOutputDirectory, `wrangler-output-${Date.now()}-${process.pid}.ndjson`);
|
|
791
|
+
await deps.fs.mkdir(wranglerOutputDirectory, { recursive: true });
|
|
792
|
+
const wranglerCommand = localWranglerExecutable ? "node" : "bunx";
|
|
793
|
+
const wranglerArgs = preview ? localWranglerExecutable ? [localWranglerExecutable, "versions", "upload"] : ["wrangler", "versions", "upload"] : localWranglerExecutable ? [localWranglerExecutable, "deploy"] : ["wrangler", "deploy"];
|
|
794
|
+
wranglerArgs.push("--config", prepared.deployConfigPath);
|
|
795
|
+
if (deployMessage?.trim()) {
|
|
796
|
+
wranglerArgs.push("--message", deployMessage.trim());
|
|
797
|
+
}
|
|
798
|
+
if (deployTag?.trim()) {
|
|
799
|
+
wranglerArgs.push("--tag", deployTag.trim());
|
|
800
|
+
}
|
|
801
|
+
const deployProc = await deps.exec.exec(wranglerCommand, wranglerArgs, {
|
|
802
|
+
cwd,
|
|
803
|
+
stdio: "inherit",
|
|
804
|
+
env: {
|
|
805
|
+
...process.env,
|
|
806
|
+
WRANGLER_OUTPUT_FILE_PATH: wranglerOutputFilePath,
|
|
807
|
+
FORCE_COLOR: process.env.FORCE_COLOR ?? "0"
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
let structuredOutput = "";
|
|
811
|
+
try {
|
|
812
|
+
structuredOutput = await deps.fs.readFile(wranglerOutputFilePath, "utf8");
|
|
813
|
+
} catch {
|
|
814
|
+
structuredOutput = "";
|
|
815
|
+
} finally {
|
|
816
|
+
try {
|
|
817
|
+
await deps.fs.unlink(wranglerOutputFilePath);
|
|
818
|
+
} catch {}
|
|
819
|
+
}
|
|
820
|
+
const parsedConsoleOutput = parseWranglerDeployOutput([deployProc.stdout, deployProc.stderr].filter((value) => typeof value === "string" && value.length > 0).join(`
|
|
821
|
+
`));
|
|
822
|
+
const parsedStructuredOutput = structuredOutput ? parseWranglerStructuredOutput(structuredOutput) : { urls: [], versionId: undefined, previewUrl: undefined };
|
|
823
|
+
const parsedOutput = mergeParsedWranglerDeployOutputs(parsedConsoleOutput, parsedStructuredOutput);
|
|
824
|
+
const workersDevUrl = parsedOutput.urls.find((url) => url.includes("workers.dev"));
|
|
825
|
+
const configuredAccountId = normalizeCloudflareAccountId(prepared.config.accountId) ?? normalizeCloudflareAccountId(process.env.CLOUDFLARE_ACCOUNT_ID);
|
|
826
|
+
let resolvedAccountId = configuredAccountId;
|
|
827
|
+
let didAttemptAccountResolution = false;
|
|
828
|
+
const versionRecoveryDiagnostics = [];
|
|
829
|
+
const ensureResolvedAccountId = async () => {
|
|
830
|
+
if (resolvedAccountId || didAttemptAccountResolution) {
|
|
831
|
+
return resolvedAccountId;
|
|
832
|
+
}
|
|
833
|
+
didAttemptAccountResolution = true;
|
|
834
|
+
resolvedAccountId = await resolveDeployAccountId(undefined);
|
|
835
|
+
return resolvedAccountId;
|
|
836
|
+
};
|
|
837
|
+
let resolvedVersionId = parsedOutput.versionId;
|
|
838
|
+
let resolvedPreviewUrl = parsedOutput.previewUrl;
|
|
839
|
+
let loggedVersionId = false;
|
|
840
|
+
let verificationNote;
|
|
841
|
+
const persistDeployMetadata = async (input) => {
|
|
842
|
+
await writeDeployResultMetadata({
|
|
843
|
+
status: input.status,
|
|
844
|
+
exitCode: input.exitCode,
|
|
845
|
+
workerName: prepared.config.name,
|
|
846
|
+
preview,
|
|
847
|
+
branchScopedPreview: isBranchScopedPreviewDeployment,
|
|
848
|
+
previewScope: resolvedPreviewScopeName,
|
|
849
|
+
versionId: resolvedVersionId,
|
|
850
|
+
previewUrl: resolvedPreviewUrl,
|
|
851
|
+
workersDevUrl,
|
|
852
|
+
verificationNote,
|
|
853
|
+
outputUrls: parsedOutput.urls,
|
|
854
|
+
structuredOutput,
|
|
855
|
+
...input.error ? { error: input.error } : {}
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
if (deployProc.exitCode !== 0) {
|
|
859
|
+
await persistDeployMetadata({
|
|
860
|
+
status: "failure",
|
|
861
|
+
exitCode: 1,
|
|
862
|
+
error: deployProc.stderr || deployProc.stdout || "Wrangler deploy failed"
|
|
863
|
+
});
|
|
864
|
+
logger.error("Deployment failed");
|
|
865
|
+
return { exitCode: 1, output: structuredOutput };
|
|
866
|
+
}
|
|
867
|
+
if (!preview && !resolvedVersionId && !isBranchScopedPreviewDeployment) {
|
|
868
|
+
resolvedAccountId = await ensureResolvedAccountId();
|
|
869
|
+
}
|
|
870
|
+
if (isBranchScopedPreviewDeployment && !resolvedPreviewUrl) {
|
|
871
|
+
resolvedAccountId = await ensureResolvedAccountId();
|
|
872
|
+
}
|
|
873
|
+
if (isBranchScopedPreviewDeployment && !resolvedPreviewUrl && resolvedAccountId) {
|
|
874
|
+
const workersSubdomain = await getWorkersSubdomain(resolvedAccountId);
|
|
875
|
+
if (workersSubdomain) {
|
|
876
|
+
resolvedPreviewUrl = formatWorkersDevUrl(prepared.config.name, workersSubdomain);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (!resolvedVersionId && resolvedAccountId) {
|
|
880
|
+
try {
|
|
881
|
+
resolvedVersionId = await resolveVersionIdFromLatestWorkerVersion({
|
|
882
|
+
accountId: resolvedAccountId,
|
|
883
|
+
workerName: prepared.config.name,
|
|
884
|
+
preview: preview || isBranchScopedPreviewDeployment,
|
|
885
|
+
deployedAfter: deployStartedAt
|
|
886
|
+
});
|
|
887
|
+
logger.success(`Version ID: ${resolvedVersionId}`);
|
|
888
|
+
loggedVersionId = true;
|
|
889
|
+
logLine(logger, dim("Resolved version id from Cloudflare version metadata", theme));
|
|
890
|
+
} catch (error) {
|
|
891
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
892
|
+
versionRecoveryDiagnostics.push(`version lookup: ${message}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (isBranchScopedPreviewDeployment && !resolvedVersionId && resolvedAccountId) {
|
|
896
|
+
try {
|
|
897
|
+
const fallbackDeployment = await resolveVersionIdFromLatestProductionDeployment({
|
|
898
|
+
accountId: resolvedAccountId,
|
|
899
|
+
workerName: prepared.config.name,
|
|
900
|
+
deployedAfter: deployStartedAt
|
|
901
|
+
});
|
|
902
|
+
resolvedVersionId = fallbackDeployment.versionId;
|
|
903
|
+
logger.success(`Version ID: ${resolvedVersionId}`);
|
|
904
|
+
loggedVersionId = true;
|
|
905
|
+
logLine(logger, dim(`Resolved version id from Cloudflare deployment ${fallbackDeployment.deploymentId}`, theme));
|
|
906
|
+
} catch (error) {
|
|
907
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
908
|
+
versionRecoveryDiagnostics.push(`deployment lookup: ${message}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (!preview && !isBranchScopedPreviewDeployment && !resolvedVersionId && resolvedAccountId) {
|
|
912
|
+
try {
|
|
913
|
+
const fallbackDeployment = await resolveVersionIdFromLatestProductionDeployment({
|
|
914
|
+
accountId: resolvedAccountId,
|
|
915
|
+
workerName: prepared.config.name,
|
|
916
|
+
deployedAfter: deployStartedAt
|
|
917
|
+
});
|
|
918
|
+
resolvedVersionId = fallbackDeployment.versionId;
|
|
919
|
+
logger.success(`Version ID: ${resolvedVersionId}`);
|
|
920
|
+
loggedVersionId = true;
|
|
921
|
+
logLine(logger, dim(`Resolved version id from Cloudflare deployment ${fallbackDeployment.deploymentId}`, theme));
|
|
922
|
+
} catch (error) {
|
|
923
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
924
|
+
versionRecoveryDiagnostics.push(`deployment lookup: ${message}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (!preview && !isBranchScopedPreviewDeployment && !resolvedVersionId && resolvedAccountId) {
|
|
928
|
+
try {
|
|
929
|
+
const currentDeployment = await resolveVersionIdFromCurrentProductionDeployment({
|
|
930
|
+
accountId: resolvedAccountId,
|
|
931
|
+
workerName: prepared.config.name
|
|
932
|
+
});
|
|
933
|
+
resolvedVersionId = currentDeployment.versionId;
|
|
934
|
+
logger.success(`Version ID: ${resolvedVersionId}`);
|
|
935
|
+
loggedVersionId = true;
|
|
936
|
+
const reuseMessage = `Cloudflare did not expose a fresh deployment or version after verification retries, and the current active deployment ${currentDeployment.deploymentId} still points at version ${resolvedVersionId}. This usually means the built Worker code and configuration were unchanged, so Cloudflare kept the existing live version.`;
|
|
937
|
+
verificationNote = reuseMessage;
|
|
938
|
+
if (requireFreshProductionDeployment) {
|
|
939
|
+
await persistDeployMetadata({
|
|
940
|
+
status: "failure",
|
|
941
|
+
exitCode: 1,
|
|
942
|
+
error: reuseMessage
|
|
943
|
+
});
|
|
944
|
+
logger.error(`Deployment verification failed: ${reuseMessage} This run requires a fresh production deployment, so Devflare is treating the reused live version as a failure.`);
|
|
945
|
+
return { exitCode: 1, output: structuredOutput };
|
|
946
|
+
}
|
|
947
|
+
logger.warn(`Deployment verification note: ${reuseMessage}`);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
950
|
+
versionRecoveryDiagnostics.push(`current production deployment: ${message}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (resolvedVersionId && !loggedVersionId) {
|
|
954
|
+
logger.success(`Version ID: ${resolvedVersionId}`);
|
|
955
|
+
}
|
|
956
|
+
if (isBranchScopedPreviewDeployment && !resolvedPreviewUrl && resolvedAccountId) {
|
|
957
|
+
const workersSubdomain = await getWorkersSubdomain(resolvedAccountId);
|
|
958
|
+
if (workersSubdomain) {
|
|
959
|
+
resolvedPreviewUrl = formatWorkersDevUrl(prepared.config.name, workersSubdomain);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if ((preview || isBranchScopedPreviewDeployment) && resolvedPreviewUrl) {
|
|
963
|
+
logLine(logger, `Preview URL: ${resolvedPreviewUrl}`);
|
|
964
|
+
}
|
|
965
|
+
if (shouldVerifyDeployControlPlane()) {
|
|
966
|
+
if (!resolvedVersionId) {
|
|
967
|
+
const recoveryDetails = versionRecoveryDiagnostics.length > 0 ? ` Cloudflare fallback checks also failed: ${versionRecoveryDiagnostics.join(" | ")}` : "";
|
|
968
|
+
await persistDeployMetadata({
|
|
969
|
+
status: "failure",
|
|
970
|
+
exitCode: 1,
|
|
971
|
+
error: `Wrangler did not return a Worker version id, so Devflare could not prove which version Cloudflare accepted.${recoveryDetails}`
|
|
972
|
+
});
|
|
973
|
+
logger.error(`Deployment verification failed: Wrangler did not return a Worker version id, so Devflare could not prove which version Cloudflare accepted.${recoveryDetails}`);
|
|
974
|
+
return { exitCode: 1, output: structuredOutput };
|
|
975
|
+
}
|
|
976
|
+
resolvedAccountId = await ensureResolvedAccountId();
|
|
977
|
+
if (!resolvedAccountId) {
|
|
978
|
+
await persistDeployMetadata({
|
|
979
|
+
status: "failure",
|
|
980
|
+
exitCode: 1,
|
|
981
|
+
error: "Devflare could not resolve a Cloudflare account id."
|
|
982
|
+
});
|
|
983
|
+
logger.error("Deployment verification failed: Devflare could not resolve a Cloudflare account id. Pass cloudflare-account-id to the action or set accountId in devflare.config.ts.");
|
|
984
|
+
return { exitCode: 1, output: structuredOutput };
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
await verifyDeployControlPlane({
|
|
988
|
+
accountId: resolvedAccountId,
|
|
989
|
+
workerName: prepared.config.name,
|
|
990
|
+
versionId: resolvedVersionId,
|
|
991
|
+
preview,
|
|
992
|
+
logger,
|
|
993
|
+
theme
|
|
994
|
+
});
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
997
|
+
await persistDeployMetadata({
|
|
998
|
+
status: "failure",
|
|
999
|
+
exitCode: 1,
|
|
1000
|
+
error: message
|
|
1001
|
+
});
|
|
1002
|
+
logger.error(`Deployment verification failed: ${message}`);
|
|
1003
|
+
return { exitCode: 1, output: structuredOutput };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (resolvedAccountId) {
|
|
1007
|
+
const previewRegistryScope = isBranchScopedPreviewDeployment ? deployTarget.previewScope : undefined;
|
|
1008
|
+
const previewRegistryUrl = preview || isBranchScopedPreviewDeployment ? resolvedPreviewUrl : undefined;
|
|
1009
|
+
try {
|
|
1010
|
+
await reconcilePreviewRegistry({
|
|
1011
|
+
accountId: resolvedAccountId,
|
|
1012
|
+
workerName: prepared.config.name,
|
|
1013
|
+
versionId: resolvedVersionId,
|
|
1014
|
+
previewScope: previewRegistryScope,
|
|
1015
|
+
previewUrl: previewRegistryUrl,
|
|
1016
|
+
branchName: resolvedPreviewScopeName,
|
|
1017
|
+
commitSha: process.env.GITHUB_SHA,
|
|
1018
|
+
source: inferRecordSource(),
|
|
1019
|
+
deploymentMessage: process.env.GITHUB_EVENT_NAME,
|
|
1020
|
+
logger
|
|
1021
|
+
});
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1024
|
+
logger.warn(`Devflare preview registry sync failed: ${message}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
await persistDeployMetadata({
|
|
1028
|
+
status: "success",
|
|
1029
|
+
exitCode: 0
|
|
1030
|
+
});
|
|
1031
|
+
logger.success("Deployed successfully!");
|
|
1032
|
+
return { exitCode: 0, output: structuredOutput };
|
|
1033
|
+
});
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
await writeDeployResultMetadata({
|
|
1036
|
+
status: "failure",
|
|
1037
|
+
exitCode: 1,
|
|
1038
|
+
preview,
|
|
1039
|
+
branchScopedPreview: !preview && environment === "preview" && Boolean(resolvedPreviewScopeName),
|
|
1040
|
+
previewScope: resolvedPreviewScopeName,
|
|
1041
|
+
outputUrls: [],
|
|
1042
|
+
...error instanceof Error ? { error: error.message } : { error: String(error) }
|
|
1043
|
+
});
|
|
1044
|
+
if (error instanceof Error) {
|
|
1045
|
+
logger.error("Deployment failed:", error.message);
|
|
1046
|
+
if (resolvedParsed.options.debug) {
|
|
1047
|
+
logger.error(error.stack);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { exitCode: 1 };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
export {
|
|
1054
|
+
runDeployCommand
|
|
1055
|
+
};
|