Skip to content

Commit f2a16f1

Browse files
fix: setting triggers.crons:[] in Wrangler config should delete deployed cron schedules (#9266)
* test: tighten up metrics module mocking Previously there were occasionally flakes where the module appeared to be unmocked when it was still supposed to be mocked. * fix: setting triggers.crons:[] in Wrangler config should delete deployed cron schedules
1 parent 00f6956 commit f2a16f1

File tree

10 files changed

+136
-26
lines changed

10 files changed

+136
-26
lines changed

.changeset/shaky-planets-feel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: setting triggers.crons:[] in Wrangler config should delete deployed cron schedules

packages/wrangler/src/__tests__/config/configuration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe("normalizeAndValidateConfig()", () => {
114114
ai: undefined,
115115
version_metadata: undefined,
116116
triggers: {
117-
crons: [],
117+
crons: undefined,
118118
},
119119
unsafe: {
120120
bindings: undefined,

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,6 +2071,43 @@ Update them to point to this script instead?`,
20712071
it.todo("should error if it's a workers.dev route");
20722072
});
20732073

2074+
describe("triggers", () => {
2075+
it("should deploy the worker with a scheduled trigger", async () => {
2076+
const crons = ["*/5 * * * *", "0 18 * * 6L"];
2077+
writeWranglerConfig({
2078+
triggers: { crons },
2079+
});
2080+
writeWorkerSource();
2081+
mockSubDomainRequest();
2082+
mockUploadWorkerRequest({ expectedType: "esm" });
2083+
mockPublishSchedulesRequest({ crons });
2084+
await runWrangler("deploy ./index");
2085+
});
2086+
2087+
it("should deploy the worker with an empty array of scheduled triggers", async () => {
2088+
const crons: string[] = [];
2089+
writeWranglerConfig({
2090+
triggers: { crons },
2091+
});
2092+
writeWorkerSource();
2093+
mockSubDomainRequest();
2094+
mockUploadWorkerRequest({ expectedType: "esm" });
2095+
mockPublishSchedulesRequest({ crons });
2096+
await runWrangler("deploy ./index");
2097+
});
2098+
2099+
it.each([{ triggers: { crons: undefined } }, { triggers: undefined }, {}])(
2100+
"should deploy the worker without updating the scheduled triggers",
2101+
async (config) => {
2102+
writeWranglerConfig(config);
2103+
writeWorkerSource();
2104+
mockSubDomainRequest();
2105+
mockUploadWorkerRequest({ expectedType: "esm" });
2106+
await runWrangler("deploy ./index");
2107+
}
2108+
);
2109+
});
2110+
20742111
describe("entry-points", () => {
20752112
it("should be able to use `index` with no extension as the entry-point (esm)", async () => {
20762113
writeWranglerConfig();
@@ -12945,6 +12982,38 @@ function mockLastDeploymentRequest() {
1294512982
msw.use(...mswSuccessDeploymentScriptMetadata);
1294612983
}
1294712984

12985+
function mockPublishSchedulesRequest({
12986+
crons = [],
12987+
env = undefined,
12988+
legacyEnv = false,
12989+
}: {
12990+
crons: Config["triggers"]["crons"];
12991+
env?: string | undefined;
12992+
legacyEnv?: boolean | undefined;
12993+
}) {
12994+
const servicesOrScripts = env && !legacyEnv ? "services" : "scripts";
12995+
const environment = env && !legacyEnv ? "/environments/:envName" : "";
12996+
12997+
msw.use(
12998+
http.put(
12999+
`*/accounts/:accountId/workers/${servicesOrScripts}/:scriptName${environment}/schedules`,
13000+
async ({ request, params }) => {
13001+
expect(params.accountId).toEqual("some-account-id");
13002+
expect(params.scriptName).toEqual(
13003+
legacyEnv && env ? `test-name-${env}` : "test-name"
13004+
);
13005+
if (!legacyEnv) {
13006+
expect(params.envName).toEqual(env);
13007+
}
13008+
const body = (await request.json()) as [{ cron: string }];
13009+
expect(body).toEqual(crons.map((cron) => ({ cron })));
13010+
return HttpResponse.json(createFetchResult(null));
13011+
},
13012+
{ once: true }
13013+
)
13014+
);
13015+
}
13016+
1294813017
function mockPublishRoutesRequest({
1294913018
routes = [],
1295013019
env = undefined,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ import { writeWranglerConfig } from "./helpers/write-wrangler-config";
3030
import type { MockInstance } from "vitest";
3131

3232
vi.mock("../metrics/helpers");
33-
vi.unmock("../metrics/metrics-config");
3433
vi.mock("../metrics/send-event");
3534
vi.mock("../package-manager");
35+
vi.mocked(getMetricsConfig).mockReset();
3636

3737
// eslint-disable-next-line @typescript-eslint/no-namespace
3838
declare module globalThis {
@@ -44,7 +44,7 @@ describe("metrics", () => {
4444
let isCISpy: MockInstance;
4545
const std = mockConsoleMethods();
4646
const { setIsTTY } = useMockIsTTY();
47-
runInTempDir();
47+
runInTempDir({ homedir: "foo" });
4848

4949
beforeEach(async () => {
5050
isCISpy = vi.spyOn(CI, "isCI").mockReturnValue(false);

packages/wrangler/src/__tests__/vitest.setup.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,16 @@ vi.mock("xdg-app-paths", () => {
162162
vi.mock("../metrics/metrics-config", async (importOriginal) => {
163163
const realModule =
164164
await importOriginal<typeof import("../metrics/metrics-config")>();
165-
const fakeModule = {
166-
...realModule,
167-
getMetricsConfig: () => async () => {
168-
return {
169-
enabled: false,
170-
deviceId: "mock-device",
171-
userId: undefined,
172-
};
173-
},
174-
};
175-
return fakeModule;
165+
vi.spyOn(realModule, "getMetricsConfig").mockImplementation(() => {
166+
return {
167+
enabled: false,
168+
deviceId: "mock-device",
169+
userId: undefined,
170+
};
171+
});
172+
return realModule;
176173
});
174+
177175
vi.mock("prompts", () => {
178176
return {
179177
__esModule: true,

packages/wrangler/src/config/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ export const defaultWranglerConfig: Config = {
332332
jsx_fragment: "React.Fragment",
333333
migrations: [],
334334
triggers: {
335-
crons: [],
335+
crons: undefined,
336336
},
337337
rules: [],
338338
build: { command: undefined, watch_dir: "./src", cwd: undefined },

packages/wrangler/src/config/environment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,10 @@ interface EnvironmentInheritable {
285285
*
286286
* For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#triggers
287287
*
288-
* @default {crons:[]}
288+
* @default {crons: undefined}
289289
* @inheritable
290290
*/
291-
triggers: { crons: string[] };
291+
triggers: { crons: string[] | undefined };
292292

293293
/**
294294
* Specify limits for runtime behavior.

packages/wrangler/src/config/validation.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
inheritableInLegacyEnvironments,
1818
isBoolean,
1919
isMutuallyExclusiveWith,
20-
isObjectWith,
2120
isOptionalProperty,
2221
isRequiredProperty,
2322
isString,
@@ -1122,8 +1121,8 @@ function normalizeAndValidateEnvironment(
11221121
topLevelEnv,
11231122
rawEnv,
11241123
"triggers",
1125-
isObjectWith("crons"),
1126-
{ crons: [] }
1124+
validateTriggers,
1125+
{ crons: undefined }
11271126
),
11281127
assets: normalizeAndValidateAssets(diagnostics, topLevelEnv, rawEnv),
11291128
limits: normalizeAndValidateLimits(diagnostics, topLevelEnv, rawEnv),
@@ -1509,6 +1508,44 @@ const validateAndNormalizeRules = (
15091508
);
15101509
};
15111510

1511+
const validateTriggers: ValidatorFn = (
1512+
diagnostics,
1513+
triggersFieldName,
1514+
triggersValue
1515+
) => {
1516+
if (triggersValue === undefined || triggersValue === null) {
1517+
return true;
1518+
}
1519+
1520+
if (typeof triggersValue !== "object") {
1521+
diagnostics.errors.push(
1522+
`Expected "${triggersFieldName}" to be of type object but got ${JSON.stringify(
1523+
triggersValue
1524+
)}.`
1525+
);
1526+
return false;
1527+
}
1528+
1529+
let isValid = true;
1530+
1531+
if ("crons" in triggersValue && !Array.isArray(triggersValue.crons)) {
1532+
diagnostics.errors.push(
1533+
`Expected "${triggersFieldName}.crons" to be of type array, but got ${JSON.stringify(triggersValue)}.`
1534+
);
1535+
isValid = false;
1536+
}
1537+
1538+
isValid =
1539+
validateAdditionalProperties(
1540+
diagnostics,
1541+
triggersFieldName,
1542+
Object.keys(triggersValue),
1543+
["crons"]
1544+
) && isValid;
1545+
1546+
return isValid;
1547+
};
1548+
15121549
const validateRules =
15131550
(envName: string): ValidatorFn =>
15141551
(diagnostics, field, envValue, config) => {

packages/wrangler/src/dev/miniflare.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ export async function buildMiniflareOptions(
11381138
internalObjects: CfDurableObject[];
11391139
entrypointNames: string[];
11401140
}> {
1141-
if (config.crons.length > 0 && !config.testScheduled) {
1141+
if (config.crons?.length && !config.testScheduled) {
11421142
if (!didWarnMiniflareCronSupport) {
11431143
didWarnMiniflareCronSupport = true;
11441144
logger.warn(

packages/wrangler/src/triggers/deploy.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default async function triggersDeploy(
3838
): Promise<string[] | void> {
3939
const { config, accountId, name: scriptName } = props;
4040

41-
const triggers = props.triggers || config.triggers?.crons;
41+
const schedules = props.triggers || config.triggers?.crons;
4242
const routes =
4343
props.routes ?? config.routes ?? (config.route ? [config.route] : []) ?? [];
4444
const routesOnly: Array<Route> = [];
@@ -258,17 +258,18 @@ export default async function triggersDeploy(
258258
}
259259

260260
// Configure any schedules for the script.
261-
// TODO: rename this to `schedules`?
262-
if (triggers && triggers.length) {
261+
// If schedules is not defined then we just leave whatever is previously deployed alone.
262+
// If it is an empty array we will remove all schedules.
263+
if (schedules) {
263264
deployments.push(
264265
fetchResult(`${workerUrl}/schedules`, {
265266
// Note: PUT will override previous schedules on this script.
266267
method: "PUT",
267-
body: JSON.stringify(triggers.map((cron) => ({ cron }))),
268+
body: JSON.stringify(schedules.map((cron) => ({ cron }))),
268269
headers: {
269270
"Content-Type": "application/json",
270271
},
271-
}).then(() => triggers.map((trigger) => `schedule: ${trigger}`))
272+
}).then(() => schedules.map((trigger) => `schedule: ${trigger}`))
272273
);
273274
}
274275

0 commit comments

Comments
 (0)