Skip to content

Commit a18b1a8

Browse files
Add strict mode for the wrangler deploy command
1 parent 4492eb0 commit a18b1a8

File tree

4 files changed

+143
-8
lines changed

4 files changed

+143
-8
lines changed

.changeset/calm-camels-return.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add strict mode for the `wrangler deploy` command
6+
7+
Add a new flag: `--strict` that makes the `wrangler deploy` command be more strict/prudent and not deploy workers when such deployments can be potentially problematic. This "strict mode" currently only effects non-interactive sessions where conflicts with the remote settings for the worker (for example when the worker has been re-deployed via the dashboard) will cause the deployment to fail instead of automatically overriding the remote settings.

packages/wrangler/src/__tests__/deploy.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13112,6 +13112,96 @@ export default{
1311213112
return normalizedLog;
1311313113
}
1311413114
});
13115+
13116+
describe("with strict mode enabled", () => {
13117+
it("should error if there are remote config difference (with --x-remote-diff-check) in non-interactive mode", async () => {
13118+
setIsTTY(false);
13119+
13120+
writeWorkerSource();
13121+
mockGetServiceByName("test-name", "production", "dash");
13122+
writeWranglerConfig(
13123+
{
13124+
compatibility_date: "2024-04-24",
13125+
main: "./index.js",
13126+
},
13127+
"./wrangler.json"
13128+
);
13129+
mockSubDomainRequest();
13130+
mockUploadWorkerRequest();
13131+
mockGetServiceBindings("test-name", []);
13132+
mockGetServiceRoutes("test-name", []);
13133+
mockGetServiceCustomDomainRecords([]);
13134+
mockGetServiceSubDomainData("test-name", { enabled: true });
13135+
mockGetServiceSchedules("test-name", { schedules: [] });
13136+
mockGetServiceMetadata("test-name", {
13137+
created_on: "2025-08-07T09:34:47.846308Z",
13138+
modified_on: "2025-08-08T10:48:12.688997Z",
13139+
script: {
13140+
created_on: "2025-08-07T09:34:47.846308Z",
13141+
modified_on: "2025-08-08T10:48:12.688997Z",
13142+
id: "silent-firefly-dbe3",
13143+
observability: { enabled: true, head_sampling_rate: 1 },
13144+
compatibility_date: "2024-04-24",
13145+
},
13146+
} as unknown as ServiceMetadataRes["default_environment"]);
13147+
13148+
await runWrangler("deploy --x-remote-diff-check --strict");
13149+
13150+
expect(std.warn).toMatchInlineSnapshot(`
13151+
"▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard:
13152+
13153+
\\"bindings\\": []
13154+
},
13155+
\\"observability\\": {
13156+
- \\"enabled\\": true,
13157+
+ \\"enabled\\": false,
13158+
\\"head_sampling_rate\\": 1,
13159+
\\"logs\\": {
13160+
\\"enabled\\": false,
13161+
13162+
Deploying the Worker will override the remote configuration with your local one.
13163+
13164+
"
13165+
`);
13166+
13167+
expect(std.err).toMatchInlineSnapshot(`
13168+
"X [ERROR] Aborting the deployment operation (due to strict mode, to prevent this failure either remove the \`--strict\` flag or add the \`--force\` one)
13169+
13170+
"
13171+
`);
13172+
// note: the test and the wrangler run share the same process, and we expect the deploy command (which fails)
13173+
// to set a non-zero exit code
13174+
expect(process.exitCode).not.toBe(0);
13175+
});
13176+
13177+
it("should error when worker was last deployed from api", async () => {
13178+
setIsTTY(false);
13179+
13180+
msw.use(...mswSuccessDeploymentScriptAPI);
13181+
writeWranglerConfig();
13182+
writeWorkerSource();
13183+
mockSubDomainRequest();
13184+
mockUploadWorkerRequest();
13185+
13186+
await runWrangler("deploy ./index --strict");
13187+
13188+
expect(std.warn).toMatchInlineSnapshot(`
13189+
"▲ [WARNING] You are about to publish a Workers Service that was last updated via the script API.
13190+
13191+
Edits that have been made via the script API will be overridden by your local code and config.
13192+
13193+
"
13194+
`);
13195+
expect(std.err).toMatchInlineSnapshot(`
13196+
"X [ERROR] Aborting the deployment operation (due to strict mode, to prevent this failure either remove the \`--strict\` flag or add the \`--force\` one)
13197+
13198+
"
13199+
`);
13200+
// note: the test and the wrangler run share the same process, and we expect the deploy command (which fails)
13201+
// to set a non-zero exit code
13202+
expect(process.exitCode).not.toBe(0);
13203+
});
13204+
});
1311513205
});
1311613206

1311713207
/** Write mock assets to the file system so they can be uploaded. */

packages/wrangler/src/deploy/deploy.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ import {
2323
import { noBundleWorker } from "../deployment-bundle/no-bundle-worker";
2424
import { validateNodeCompatMode } from "../deployment-bundle/node-compat";
2525
import { loadSourceMaps } from "../deployment-bundle/source-maps";
26-
import { confirm } from "../dialogs";
26+
import { confirm as genericConfirm } from "../dialogs";
2727
import { getMigrationsToUpload } from "../durable";
2828
import { getDockerPath } from "../environment-variables/misc-variables";
2929
import { UserError } from "../errors";
3030
import { getFlag } from "../experimental-flags";
3131
import { downloadWorkerConfig } from "../init";
32+
import { isNonInteractiveOrCI } from "../is-interactive";
3233
import { logger } from "../logger";
3334
import { getMetricsUsageHeaders } from "../metrics";
3435
import { isNavigatorDefined } from "../navigator-user-agent";
@@ -115,6 +116,8 @@ type Props = {
115116
experimentalAutoCreate: boolean;
116117
metafile: string | boolean | undefined;
117118
containersRollout: "immediate" | "gradual" | undefined;
119+
strict: boolean | undefined;
120+
force: boolean | undefined;
118121
};
119122

120123
export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
@@ -314,7 +317,7 @@ export async function publishCustomDomains(
314317
const message = `Custom Domains already exist for these domains:
315318
${existingRendered}
316319
Update them to point to this script instead?`;
317-
if (!(await confirm(message))) {
320+
if (!(await genericConfirm(message))) {
318321
return fail();
319322
}
320323
options.override_existing_origin = true;
@@ -327,7 +330,7 @@ Update them to point to this script instead?`;
327330
const message = `You already have DNS records that conflict for these Custom Domains:
328331
${conflicitingRendered}
329332
Update them to point to this script instead?`;
330-
if (!(await confirm(message))) {
333+
if (!(await genericConfirm(message))) {
331334
return fail();
332335
}
333336
options.override_existing_dns_record = true;
@@ -352,6 +355,8 @@ export default async function deploy(props: Props): Promise<{
352355
workerTag: string | null;
353356
targets?: string[];
354357
}> {
358+
const deployConfirm = getDeployConfirmFunction(props.strict && !props.force);
359+
355360
// TODO: warn if git/hg has uncommitted changes
356361
const { config, accountId, name, entry } = props;
357362
let workerTag: string | null = null;
@@ -409,23 +414,23 @@ export default async function deploy(props: Props): Promise<{
409414
`\n${configDiff.diff}\n\n` +
410415
"Deploying the Worker will override the remote configuration with your local one."
411416
);
412-
if (!(await confirm("Would you like to continue?"))) {
417+
if (!(await deployConfirm("Would you like to continue?"))) {
413418
return { versionId, workerTag };
414419
}
415420
}
416421
} else {
417422
logger.warn(
418423
`You are about to publish a Workers Service that was last published via the Cloudflare Dashboard.\nEdits that have been made via the dashboard will be overridden by your local code and config.`
419424
);
420-
if (!(await confirm("Would you like to continue?"))) {
425+
if (!(await deployConfirm("Would you like to continue?"))) {
421426
return { versionId, workerTag };
422427
}
423428
}
424429
} else if (script.last_deployed_from === "api") {
425430
logger.warn(
426431
`You are about to publish a Workers Service that was last updated via the script API.\nEdits that have been made via the script API will be overridden by your local code and config.`
427432
);
428-
if (!(await confirm("Would you like to continue?"))) {
433+
if (!(await deployConfirm("Would you like to continue?"))) {
429434
return { versionId, workerTag };
430435
}
431436
}
@@ -1396,3 +1401,21 @@ export async function updateQueueConsumers(
13961401

13971402
return updateConsumers;
13981403
}
1404+
1405+
function getDeployConfirmFunction(
1406+
strictMode = false
1407+
): (text: string) => Promise<boolean> {
1408+
const nonInteractive = isNonInteractiveOrCI();
1409+
1410+
if (nonInteractive && strictMode) {
1411+
return () => {
1412+
logger.error(
1413+
"Aborting the deployment operation (due to strict mode, to prevent this failure either remove the `--strict` flag or add the `--force` one)"
1414+
);
1415+
process.exitCode = 1;
1416+
return Promise.resolve(false);
1417+
};
1418+
}
1419+
1420+
return genericConfirm;
1421+
}

packages/wrangler/src/deploy/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,24 @@ export const deployCommand = createCommand({
232232
choices: ["immediate", "gradual"] as const,
233233
},
234234
"experimental-deploy-remote-diff-check": {
235-
describe: `Experimental: Enable The Deployment Remote Diff check`,
235+
describe: "Experimental: Enable The Deployment Remote Diff check",
236236
type: "boolean",
237237
hidden: true,
238238
alias: ["x-remote-diff-check"],
239239
},
240+
strict: {
241+
describe:
242+
"Enables strict mode for the deploy command, this prevents deployments to occur when there are even small potential risks.",
243+
type: "boolean",
244+
default: false,
245+
},
246+
// TODO: check, if `--force` really necessary? users can just provide or not `--strict`, no?
247+
force: {
248+
describe:
249+
"This flag can be used to disable strict mode (if set via `--strict`).",
250+
type: "boolean",
251+
default: false,
252+
},
240253
},
241254
behaviour: {
242255
useConfigRedirectIfAvailable: true,
@@ -251,7 +264,7 @@ export const deployCommand = createCommand({
251264
validateArgs(args) {
252265
if (args.nodeCompat) {
253266
throw new UserError(
254-
`The --node-compat flag is no longer supported as of Wrangler v4. Instead, use the \`nodejs_compat\` compatibility flag. This includes the functionality from legacy \`node_compat\` polyfills and natively implemented Node.js APIs. See https://developers.cloudflare.com/workers/runtime-apis/nodejs for more information.`,
267+
"The --node-compat flag is no longer supported as of Wrangler v4. Instead, use the `nodejs_compat` compatibility flag. This includes the functionality from legacy `node_compat` polyfills and natively implemented Node.js APIs. See https://developers.cloudflare.com/workers/runtime-apis/nodejs for more information.",
255268
{ telemetryMessage: true }
256269
);
257270
}
@@ -382,6 +395,8 @@ export const deployCommand = createCommand({
382395
dispatchNamespace: args.dispatchNamespace,
383396
experimentalAutoCreate: args.experimentalAutoCreate,
384397
containersRollout: args.containersRollout,
398+
strict: args.strict,
399+
force: args.force,
385400
});
386401

387402
writeOutput({

0 commit comments

Comments
 (0)