Skip to content

Commit 43903a3

Browse files
Support CLOUDFLARE_ENV environment variable for selecting the active environment (#11228)
* Make BundleController tests able to run on non-unix OSs * ignore generated files from eslint * fix home directory mocking * Improve isolation of containers push tests * use logger.loggerLevel rather than recomputing it in Wrangler The startup-profiling test was leaking the logger level between tests, so that got fixed in here too. * move environment variable handling from `wrangler` to `@cloudflare/workers-utils` * Support `CLOUDFLARE_ENV` environment variable for selecting the active environment This change enables users to select the environment for commands such as `CLOUDFLARE_ENV=prod wrangler versions upload`. The `--env` command line argument takes precedence. The `CLOUDFLARE_ENV` enviroment variable is mostly used with the `@cloudflare/vite-plugin` to select the environment for building the Worker to be deployed. This build also generates a "redirected deploy config" that is flattened to only contain the active environment. To avoid accidentally deploying a version that is built for one environment to a different environment, there is an additional check to ensure that if the user specifies an environment in Wrangler it matches the original selected environment from the build. * fix error message when both CLOUDFLARE_ENV and --env are provided * fix "via" part of the validation error * fixups after James' review * send the errors to Sentry so we can ascertain how often this occurs * fixups from Dario
1 parent 235142e commit 43903a3

File tree

114 files changed

+572
-276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+572
-276
lines changed

.changeset/soft-rockets-draw.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@cloudflare/workers-utils": minor
3+
"wrangler": minor
4+
---
5+
6+
Support `CLOUDFLARE_ENV` environment variable for selecting the active environment
7+
8+
This change enables users to select the environment for commands such as `CLOUDFLARE_ENV=prod wrangler versions upload`. The `--env` command line argument takes precedence.
9+
10+
The `CLOUDFLARE_ENV` environment variable is mostly used with the `@cloudflare/vite-plugin` to select the environment for building the Worker to be deployed. This build also generates a "redirected deploy config" that is flattened to only contain the active environment.
11+
To avoid accidentally deploying a version that is built for one environment to a different environment, there is an additional check to ensure that if the user specifies an environment in Wrangler it matches the original selected environment from the build.

fixtures/redirected-config-worker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "node -r esbuild-register tools/build.ts",
1010
"check:type": "tsc",
1111
"dev": "pnpm run build && wrangler dev",
12-
"test:ci": "pnpm run build && vitest run"
12+
"test:ci": "vitest run"
1313
},
1414
"devDependencies": {
1515
"@cloudflare/workers-tsconfig": "workspace:^",

fixtures/redirected-config-worker/tests/index.test.ts

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { execSync } from "child_process";
12
import { resolve } from "path";
23
import { fetch } from "undici";
34
import { describe, it } from "vitest";
45
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
56

67
const basePath = resolve(__dirname, "..");
78

8-
describe("'wrangler dev' correctly renders pages", () => {
9+
describe("'wrangler dev', when reading redirected config,", () => {
910
it("uses the generated config", async ({ expect, onTestFinished }) => {
11+
build("prod");
1012
const { ip, port, stop } = await runWranglerDev(basePath, [
1113
"--port=0",
1214
"--inspector-port=0",
@@ -17,20 +19,76 @@ describe("'wrangler dev' correctly renders pages", () => {
1719
const response = await fetch(`http://${ip}:${port}/`);
1820
const text = await response.text();
1921
expect(response.status).toBe(200);
20-
expect(text).toMatchInlineSnapshot(`"Generated: true"`);
22+
expect(text).toMatchInlineSnapshot(`"Generated: prod"`);
2123
});
2224

23-
it("specifying an environment causes an error since they are not supported in redirected configs", async ({
25+
it("works when specifying the same environment via CLI arg to the one used in build", async ({
2426
expect,
27+
onTestFinished,
2528
}) => {
26-
await expect(
27-
runWranglerDev(basePath, [
28-
"--port=0",
29-
"--inspector-port=0",
30-
"--env=staging",
31-
])
32-
).rejects.toThrowError(
33-
/You have specified the environment ".*?", but are using a redirected configuration/
29+
build("production");
30+
31+
const { ip, port, stop } = await runWranglerDev(basePath, [
32+
"--port=0",
33+
"--inspector-port=0",
34+
"--env=production",
35+
]);
36+
onTestFinished(async () => await stop?.());
37+
38+
const response = await fetch(`http://${ip}:${port}/`);
39+
const text = await response.text();
40+
expect(response.status).toBe(200);
41+
expect(text).toMatchInlineSnapshot(`"Generated: production"`);
42+
});
43+
44+
it("errors when specifying a different environment via CLI arg to the one used in build", async ({
45+
expect,
46+
}) => {
47+
build("production");
48+
49+
const error = await runWranglerDev(basePath, [
50+
"--port=0",
51+
"--inspector-port=0",
52+
"--env=staging",
53+
]).then(
54+
// it is doesn't error then stop the process
55+
({ stop }) => stop(),
56+
(e) => e
57+
);
58+
59+
expect(error).toMatch(
60+
'You have specified the environment "staging" via the `--env/-e` CLI argument.'
61+
);
62+
expect(error).toMatch(
63+
'This does not match the target environment "production" that was used when building the application.'
64+
);
65+
expect(error).toMatch(
66+
'Perhaps you need to re-run the custom build of the project with "staging" as the selected environment?'
67+
);
68+
});
69+
70+
it("errors when specifying a different environment via CLOUDFLARE_ENV to the one used in build", async ({
71+
expect,
72+
}) => {
73+
build("production");
74+
75+
let error = "";
76+
try {
77+
await runWranglerDev(basePath, ["--port=0", "--inspector-port=0"], {
78+
CLOUDFLARE_ENV: "staging",
79+
});
80+
} catch (e) {
81+
error = e as string;
82+
}
83+
84+
expect(error).toMatch(
85+
'You have specified the environment "staging" via the CLOUDFLARE_ENV environment variable.'
86+
);
87+
expect(error).toMatch(
88+
'This does not match the target environment "production" that was used when building the application.'
89+
);
90+
expect(error).toMatch(
91+
'Perhaps you need to re-run the custom build of the project with "staging" as the selected environment?'
3492
);
3593
});
3694

@@ -52,3 +110,10 @@ describe("'wrangler dev' correctly renders pages", () => {
52110
expect(text).toMatchInlineSnapshot(`"Generated: undefined"`);
53111
});
54112
});
113+
114+
function build(env: string) {
115+
execSync("node -r esbuild-register tools/build.ts", {
116+
cwd: basePath,
117+
env: { ...process.env, CLOUDFLARE_ENV: env },
118+
});
119+
}

fixtures/redirected-config-worker/tools/build.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const config = {
77
name: "redirected-config-worker",
88
compatibility_date: "2024-12-01",
99
main: "index.js",
10-
vars: { generated: true },
10+
targetEnvironment: process.env.CLOUDFLARE_ENV,
11+
vars: { generated: process.env.CLOUDFLARE_ENV ?? "none" },
1112
};
1213
writeFileSync("build/wrangler.json", JSON.stringify(config, undefined, 2));
1314
copyFileSync("src/index.js", "build/index.js");

fixtures/redirected-config-worker/wrangler.jsonc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@
22
"name": "redirected-config-worker",
33
"compatibility_date": "2024-12-01",
44
"main": "src/index.js",
5+
"env": {
6+
"staging": {
7+
"vars": {
8+
"generated": "none",
9+
},
10+
},
11+
"prod": {
12+
"vars": {
13+
"generated": "none",
14+
},
15+
},
16+
},
517
}

packages/containers-shared/tests/helpers/run-in-tmp-dir.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,20 @@ export function runInTempDir({ homedir } = { homedir: "./home" }) {
2424
);
2525

2626
process.chdir(tmpDir);
27-
// eslint-disable-next-line turbo/no-undeclared-env-vars
28-
process.env.PWD = tmpDir;
29-
30-
// The path that is returned from `homedir()` should be absolute.
31-
const absHomedir = path.resolve(tmpDir, homedir);
27+
vi.stubEnv("PWD", tmpDir);
3228

3329
// Override where the home directory is so that we can write our own user config,
3430
// without destroying the real thing.
31+
// The path that is returned from `homedir()` should be absolute.
32+
const absHomedir = path.resolve(tmpDir, homedir);
3533
fs.mkdirSync(absHomedir, { recursive: true });
36-
37-
// Note it is very important that we use the "default" value from "node:os" (e.g. `import os from "node:os";`)
38-
// rather than an alias to the module (e.g. `import * as os from "node:os";`).
39-
// This is because the module gets transpiled so that the "method" `homedir()` gets converted to a
40-
// getter that is not configurable (and so cannot be spied upon).
41-
vi.spyOn(os, "homedir")?.mockReturnValue(absHomedir);
34+
vi.stubEnv("HOME", absHomedir);
35+
vi.stubEnv("XDG_CONFIG_HOME", path.resolve(absHomedir, ".config"));
4236
});
4337

4438
afterEach(() => {
39+
process.chdir(originalCwd);
4540
if (fs.existsSync(tmpDir)) {
46-
process.chdir(originalCwd);
47-
// eslint-disable-next-line turbo/no-undeclared-env-vars
48-
process.env.PWD = originalCwd;
49-
5041
// Don't block on deleting the tmp dir
5142
void rm(tmpDir).catch(() => {
5243
// Best effort - try once then just move on - they are only temp files after all.

packages/workers-utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"tsdown": "^0.15.9",
5353
"tsup": "8.3.0",
5454
"typescript": "catalog:default",
55-
"vitest": "catalog:default"
55+
"vitest": "catalog:default",
56+
"xdg-app-paths": "^8.3.0"
5657
},
5758
"volta": {
5859
"extends": "../../package.json"

packages/workers-utils/src/config/validation.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33
import { isDockerfile } from "@cloudflare/containers-shared";
44
import { isValidWorkflowName } from "@cloudflare/workflows-shared/src/lib/validators";
55
import { dedent } from "ts-dedent";
6+
import { getCloudflareEnv } from "../environment-variables/misc-variables";
67
import { UserError } from "../errors";
78
import { isRedirectedRawConfig } from "./config-helpers";
89
import { Diagnostics } from "./diagnostics";
@@ -257,21 +258,32 @@ export function normalizeAndValidateConfig(
257258
);
258259
}
259260

260-
//TODO: find a better way to define the type of Args that can be passed to the normalizeAndValidateConfig()
261-
const envName = args.env;
261+
// The environment can come from the CLI args (i.e. `--env`) or from the `CLOUDFLARE_ENV` environment variable.
262+
const envName = args.env ?? getCloudflareEnv();
262263
assert(envName === undefined || typeof envName === "string");
263264

264265
let activeEnv = topLevelEnv;
265266

266267
if (envName) {
267268
if (isRedirectedConfig) {
268-
// Note: we error if the user is specifying an environment, but not for pages
269-
// commands where the environment is always set (to either "preview" or "production")
270-
if (!isPagesConfig(rawConfig)) {
271-
diagnostics.errors.push(dedent`
272-
You have specified the environment "${envName}", but are using a redirected configuration, produced by a build tool such as Vite.
273-
You need to set the environment in your build tool, rather than via Wrangler.
274-
For example, if you are using Vite, refer to these docs: https://developers.cloudflare.com/workers/vite-plugin/reference/cloudflare-environments/
269+
// Check that if we are loading a redirected config, any specified environment must match the target environment
270+
// from the original user config.
271+
// Note: we don't error for pages commands where the environment is always set (to either "preview" or "production")
272+
if (
273+
!isPagesConfig(rawConfig) &&
274+
rawConfig.targetEnvironment &&
275+
rawConfig.targetEnvironment !== envName
276+
) {
277+
const via =
278+
args.env !== undefined
279+
? "via the `--env/-e` CLI argument"
280+
: "via the CLOUDFLARE_ENV environment variable";
281+
// We are throwing here rather than just adding to the diagnostics because this is a hard error
282+
// and we'd like to collect Sentry data on when and how often this is happening.
283+
throw new Error(dedent`
284+
You have specified the environment "${envName}" ${via}.
285+
This does not match the target environment "${rawConfig.targetEnvironment}" that was used when building the application.
286+
Perhaps you need to re-run the custom build of the project with "${envName}" as the selected environment?
275287
`);
276288
}
277289
} else {

packages/wrangler/src/environment-variables/factory.ts renamed to packages/workers-utils/src/environment-variables/factory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UserError } from "@cloudflare/workers-utils";
1+
import { UserError } from "../errors";
22

33
/**
44
* Environment variables supported by Wrangler for configuration and authentication.
@@ -66,6 +66,8 @@ type VariableNames =
6666
| "WRANGLER_REGISTRY_PATH"
6767
/** Additional D1 location choices (internal use). */
6868
| "WRANGLER_D1_EXTRA_LOCATION_CHOICES"
69+
/** The Workers environment to target (equivalent to the `--env` CLI param) */
70+
| "CLOUDFLARE_ENV"
6971

7072
// ## Advanced Configuration
7173

packages/wrangler/src/environment-variables/misc-variables.ts renamed to packages/workers-utils/src/environment-variables/misc-variables.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import path from "node:path";
2-
import { UserError } from "@cloudflare/workers-utils";
32
import { dedent } from "ts-dedent";
3+
import { UserError } from "../errors";
44
import { getGlobalWranglerConfigPath } from "../global-wrangler-config-path";
55
import {
66
getBooleanEnvironmentVariableFactory,
77
getEnvironmentVariableFactory,
88
} from "./factory";
9-
import type { Config } from "@cloudflare/workers-utils";
9+
import type { Config } from "../config";
1010

1111
/**
1212
* `WRANGLER_C3_COMMAND` can override the command used by `wrangler init` when delegating to C3.
@@ -321,3 +321,10 @@ export const getWranglerHideBanner = getBooleanEnvironmentVariableFactory({
321321
variableName: "WRANGLER_HIDE_BANNER",
322322
defaultValue: false,
323323
});
324+
325+
/**
326+
* `CLOUDFLARE_ENV` specifies the currently selected Wrangler/Cloudflare environment.
327+
*/
328+
export const getCloudflareEnv = getEnvironmentVariableFactory({
329+
variableName: "CLOUDFLARE_ENV",
330+
});

0 commit comments

Comments
 (0)