Skip to content

Commit df3ba23

Browse files
Add strict mode for the wrangler deploy command
1 parent b421bf6 commit df3ba23

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
@@ -14021,6 +14021,96 @@ export default{
1402114021
return normalizedLog;
1402214022
}
1402314023
});
14024+
14025+
describe("with strict mode enabled", () => {
14026+
it("should error if there are remote config difference (with --x-remote-diff-check) in non-interactive mode", async () => {
14027+
setIsTTY(false);
14028+
14029+
writeWorkerSource();
14030+
mockGetServiceByName("test-name", "production", "dash");
14031+
writeWranglerConfig(
14032+
{
14033+
compatibility_date: "2024-04-24",
14034+
main: "./index.js",
14035+
},
14036+
"./wrangler.json"
14037+
);
14038+
mockSubDomainRequest();
14039+
mockUploadWorkerRequest();
14040+
mockGetServiceBindings("test-name", []);
14041+
mockGetServiceRoutes("test-name", []);
14042+
mockGetServiceCustomDomainRecords([]);
14043+
mockGetServiceSubDomainData("test-name", { enabled: true });
14044+
mockGetServiceSchedules("test-name", { schedules: [] });
14045+
mockGetServiceMetadata("test-name", {
14046+
created_on: "2025-08-07T09:34:47.846308Z",
14047+
modified_on: "2025-08-08T10:48:12.688997Z",
14048+
script: {
14049+
created_on: "2025-08-07T09:34:47.846308Z",
14050+
modified_on: "2025-08-08T10:48:12.688997Z",
14051+
id: "silent-firefly-dbe3",
14052+
observability: { enabled: true, head_sampling_rate: 1 },
14053+
compatibility_date: "2024-04-24",
14054+
},
14055+
} as unknown as ServiceMetadataRes["default_environment"]);
14056+
14057+
await runWrangler("deploy --x-remote-diff-check --strict");
14058+
14059+
expect(std.warn).toMatchInlineSnapshot(`
14060+
"▲ [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:
14061+
14062+
\\"bindings\\": []
14063+
},
14064+
\\"observability\\": {
14065+
- \\"enabled\\": true,
14066+
+ \\"enabled\\": false,
14067+
\\"head_sampling_rate\\": 1,
14068+
\\"logs\\": {
14069+
\\"enabled\\": false,
14070+
14071+
Deploying the Worker will override the remote configuration with your local one.
14072+
14073+
"
14074+
`);
14075+
14076+
expect(std.err).toMatchInlineSnapshot(`
14077+
"X [ERROR] Aborting the deployment operation (due to strict mode, to prevent this failure either remove the \`--strict\` flag or add the \`--force\` one)
14078+
14079+
"
14080+
`);
14081+
// note: the test and the wrangler run share the same process, and we expect the deploy command (which fails)
14082+
// to set a non-zero exit code
14083+
expect(process.exitCode).not.toBe(0);
14084+
});
14085+
14086+
it("should error when worker was last deployed from api", async () => {
14087+
setIsTTY(false);
14088+
14089+
msw.use(...mswSuccessDeploymentScriptAPI);
14090+
writeWranglerConfig();
14091+
writeWorkerSource();
14092+
mockSubDomainRequest();
14093+
mockUploadWorkerRequest();
14094+
14095+
await runWrangler("deploy ./index --strict");
14096+
14097+
expect(std.warn).toMatchInlineSnapshot(`
14098+
"▲ [WARNING] You are about to publish a Workers Service that was last updated via the script API.
14099+
14100+
Edits that have been made via the script API will be overridden by your local code and config.
14101+
14102+
"
14103+
`);
14104+
expect(std.err).toMatchInlineSnapshot(`
14105+
"X [ERROR] Aborting the deployment operation (due to strict mode, to prevent this failure either remove the \`--strict\` flag or add the \`--force\` one)
14106+
14107+
"
14108+
`);
14109+
// note: the test and the wrangler run share the same process, and we expect the deploy command (which fails)
14110+
// to set a non-zero exit code
14111+
expect(process.exitCode).not.toBe(0);
14112+
});
14113+
});
1402414114
});
1402514115

1402614116
/** 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,7 +23,7 @@ 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 {
@@ -33,6 +33,7 @@ import {
3333
} from "../environments";
3434
import { UserError } from "../errors";
3535
import { getFlag } from "../experimental-flags";
36+
import { isNonInteractiveOrCI } from "../is-interactive";
3637
import { logger } from "../logger";
3738
import { getMetricsUsageHeaders } from "../metrics";
3839
import { isNavigatorDefined } from "../navigator-user-agent";
@@ -120,6 +121,8 @@ type Props = {
120121
experimentalAutoCreate: boolean;
121122
metafile: string | boolean | undefined;
122123
containersRollout: "immediate" | "gradual" | undefined;
124+
strict: boolean | undefined;
125+
force: boolean | undefined;
123126
};
124127

125128
export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
@@ -319,7 +322,7 @@ export async function publishCustomDomains(
319322
const message = `Custom Domains already exist for these domains:
320323
${existingRendered}
321324
Update them to point to this script instead?`;
322-
if (!(await confirm(message))) {
325+
if (!(await genericConfirm(message))) {
323326
return fail();
324327
}
325328
options.override_existing_origin = true;
@@ -332,7 +335,7 @@ Update them to point to this script instead?`;
332335
const message = `You already have DNS records that conflict for these Custom Domains:
333336
${conflicitingRendered}
334337
Update them to point to this script instead?`;
335-
if (!(await confirm(message))) {
338+
if (!(await genericConfirm(message))) {
336339
return fail();
337340
}
338341
options.override_existing_dns_record = true;
@@ -357,6 +360,8 @@ export default async function deploy(props: Props): Promise<{
357360
workerTag: string | null;
358361
targets?: string[];
359362
}> {
363+
const deployConfirm = getDeployConfirmFunction(props.strict && !props.force);
364+
360365
// TODO: warn if git/hg has uncommitted changes
361366
const { config, accountId, name, entry } = props;
362367
let workerTag: string | null = null;
@@ -417,23 +422,23 @@ export default async function deploy(props: Props): Promise<{
417422
`\n${configDiff.diff}\n\n` +
418423
"Deploying the Worker will override the remote configuration with your local one."
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 {
425430
logger.warn(
426431
`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.`
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
}
432437
} else if (script.last_deployed_from === "api") {
433438
logger.warn(
434439
`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.`
435440
);
436-
if (!(await confirm("Would you like to continue?"))) {
441+
if (!(await deployConfirm("Would you like to continue?"))) {
437442
return { versionId, workerTag };
438443
}
439444
}
@@ -1430,3 +1435,21 @@ export async function updateQueueConsumers(
14301435

14311436
return updateConsumers;
14321437
}
1438+
1439+
function getDeployConfirmFunction(
1440+
strictMode = false
1441+
): (text: string) => Promise<boolean> {
1442+
const nonInteractive = isNonInteractiveOrCI();
1443+
1444+
if (nonInteractive && strictMode) {
1445+
return () => {
1446+
logger.error(
1447+
"Aborting the deployment operation (due to strict mode, to prevent this failure either remove the `--strict` flag or add the `--force` one)"
1448+
);
1449+
process.exitCode = 1;
1450+
return Promise.resolve(false);
1451+
};
1452+
}
1453+
1454+
return genericConfirm;
1455+
}

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)