Skip to content

Commit 8b4f24a

Browse files
V3 Backport [#9266]: fix: setting triggers.crons:[] in Wrangler config should delete deployed cron schedules (#9267)
* 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 --------- Co-authored-by: Peter Bacon Darwin <[email protected]>
1 parent b742171 commit 8b4f24a

File tree

10 files changed

+138
-26
lines changed

10 files changed

+138
-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
@@ -2051,6 +2051,43 @@ Update them to point to this script instead?`,
20512051
it.todo("should error if it's a workers.dev route");
20522052
});
20532053

2054+
describe("triggers", () => {
2055+
it("should deploy the worker with a scheduled trigger", async () => {
2056+
const crons = ["*/5 * * * *", "0 18 * * 6L"];
2057+
writeWranglerConfig({
2058+
triggers: { crons },
2059+
});
2060+
writeWorkerSource();
2061+
mockSubDomainRequest();
2062+
mockUploadWorkerRequest({ expectedType: "esm" });
2063+
mockPublishSchedulesRequest({ crons });
2064+
await runWrangler("deploy ./index");
2065+
});
2066+
2067+
it("should deploy the worker with an empty array of scheduled triggers", async () => {
2068+
const crons: string[] = [];
2069+
writeWranglerConfig({
2070+
triggers: { crons },
2071+
});
2072+
writeWorkerSource();
2073+
mockSubDomainRequest();
2074+
mockUploadWorkerRequest({ expectedType: "esm" });
2075+
mockPublishSchedulesRequest({ crons });
2076+
await runWrangler("deploy ./index");
2077+
});
2078+
2079+
it.each([{ triggers: { crons: undefined } }, { triggers: undefined }, {}])(
2080+
"should deploy the worker without updating the scheduled triggers",
2081+
async (config) => {
2082+
writeWranglerConfig(config);
2083+
writeWorkerSource();
2084+
mockSubDomainRequest();
2085+
mockUploadWorkerRequest({ expectedType: "esm" });
2086+
await runWrangler("deploy ./index");
2087+
}
2088+
);
2089+
});
2090+
20542091
describe("entry-points", () => {
20552092
it("should be able to use `index` with no extension as the entry-point (esm)", async () => {
20562093
writeWranglerConfig();
@@ -13225,6 +13262,38 @@ function mockLastDeploymentRequest() {
1322513262
msw.use(...mswSuccessDeploymentScriptMetadata);
1322613263
}
1322713264

13265+
function mockPublishSchedulesRequest({
13266+
crons = [],
13267+
env = undefined,
13268+
legacyEnv = false,
13269+
}: {
13270+
crons: Config["triggers"]["crons"];
13271+
env?: string | undefined;
13272+
legacyEnv?: boolean | undefined;
13273+
}) {
13274+
const servicesOrScripts = env && !legacyEnv ? "services" : "scripts";
13275+
const environment = env && !legacyEnv ? "/environments/:envName" : "";
13276+
13277+
msw.use(
13278+
http.put(
13279+
`*/accounts/:accountId/workers/${servicesOrScripts}/:scriptName${environment}/schedules`,
13280+
async ({ request, params }) => {
13281+
expect(params.accountId).toEqual("some-account-id");
13282+
expect(params.scriptName).toEqual(
13283+
legacyEnv && env ? `test-name-${env}` : "test-name"
13284+
);
13285+
if (!legacyEnv) {
13286+
expect(params.envName).toEqual(env);
13287+
}
13288+
const body = (await request.json()) as [{ cron: string }];
13289+
expect(body).toEqual(crons.map((cron) => ({ cron })));
13290+
return HttpResponse.json(createFetchResult(null));
13291+
},
13292+
{ once: true }
13293+
)
13294+
);
13295+
}
13296+
1322813297
function mockPublishRoutesRequest({
1322913298
routes = [],
1323013299
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
@@ -371,7 +371,7 @@ export const defaultWranglerConfig: Config = {
371371
jsx_fragment: "React.Fragment",
372372
migrations: [],
373373
triggers: {
374-
crons: [],
374+
crons: undefined,
375375
},
376376
usage_model: undefined,
377377
rules: [],

packages/wrangler/src/config/environment.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,12 @@ interface EnvironmentInheritable {
248248
*
249249
* More details here https://developers.cloudflare.com/workers/platform/cron-triggers
250250
*
251-
* @default {crons:[]}
251+
* For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#triggers
252+
*
253+
* @default {crons: undefined}
252254
* @inheritable
253255
*/
254-
triggers: { crons: string[] };
256+
triggers: { crons: string[] | undefined };
255257

256258
/**
257259
* Specifies the Usage Model for your Worker. There are two options -

packages/wrangler/src/config/validation.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
inheritableInLegacyEnvironments,
1919
isBoolean,
2020
isMutuallyExclusiveWith,
21-
isObjectWith,
2221
isOneOf,
2322
isOptionalProperty,
2423
isRequiredProperty,
@@ -1337,8 +1336,8 @@ function normalizeAndValidateEnvironment(
13371336
topLevelEnv,
13381337
rawEnv,
13391338
"triggers",
1340-
isObjectWith("crons"),
1341-
{ crons: [] }
1339+
validateTriggers,
1340+
{ crons: undefined }
13421341
),
13431342
assets: normalizeAndValidateAssets(diagnostics, topLevelEnv, rawEnv),
13441343
usage_model: inheritable(
@@ -1741,6 +1740,44 @@ const validateAndNormalizeRules = (
17411740
);
17421741
};
17431742

1743+
const validateTriggers: ValidatorFn = (
1744+
diagnostics,
1745+
triggersFieldName,
1746+
triggersValue
1747+
) => {
1748+
if (triggersValue === undefined || triggersValue === null) {
1749+
return true;
1750+
}
1751+
1752+
if (typeof triggersValue !== "object") {
1753+
diagnostics.errors.push(
1754+
`Expected "${triggersFieldName}" to be of type object but got ${JSON.stringify(
1755+
triggersValue
1756+
)}.`
1757+
);
1758+
return false;
1759+
}
1760+
1761+
let isValid = true;
1762+
1763+
if ("crons" in triggersValue && !Array.isArray(triggersValue.crons)) {
1764+
diagnostics.errors.push(
1765+
`Expected "${triggersFieldName}.crons" to be of type array, but got ${JSON.stringify(triggersValue)}.`
1766+
);
1767+
isValid = false;
1768+
}
1769+
1770+
isValid =
1771+
validateAdditionalProperties(
1772+
diagnostics,
1773+
triggersFieldName,
1774+
Object.keys(triggersValue),
1775+
["crons"]
1776+
) && isValid;
1777+
1778+
return isValid;
1779+
};
1780+
17441781
const validateRules =
17451782
(envName: string): ValidatorFn =>
17461783
(diagnostics, field, envValue, config) => {

packages/wrangler/src/dev/miniflare.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,7 @@ export async function buildMiniflareOptions(
957957
internalObjects: CfDurableObject[];
958958
entrypointNames: string[];
959959
}> {
960-
if (config.crons.length > 0 && !config.testScheduled) {
960+
if (config.crons?.length && !config.testScheduled) {
961961
if (!didWarnMiniflareCronSupport) {
962962
didWarnMiniflareCronSupport = true;
963963
log.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)