Skip to content

Commit f5f84cd

Browse files
authored
fix: process level environment variables should take precedence over env files (#32407)
1 parent 5073362 commit f5f84cd

7 files changed

Lines changed: 134 additions & 3 deletions

File tree

cli/util/watch_env_tracker.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ impl WatchEnvTracker {
114114
let key_os = OsString::from(key);
115115
let value_os = OsString::from(value);
116116

117+
// Process-level env vars should always take precedence over env files.
118+
if inner.original_env.contains_key(&key_os) {
119+
#[allow(clippy::print_stderr)]
120+
if log_level.map(|l| l >= log::Level::Debug).unwrap_or(false) {
121+
eprintln!(
122+
"{} Variable '{}' already exists in the process environment, skipping value from '{}'",
123+
colors::yellow("Debug"),
124+
key_os.to_string_lossy(),
125+
file_path.display()
126+
);
127+
}
128+
continue;
129+
}
130+
117131
// Check if this variable is already loaded from a previous file
118132
if inner.loaded_variables.contains(&key_os) {
119133
// Variable already exists from a previous file, skip it
@@ -241,16 +255,46 @@ pub fn load_env_variables_from_env_files(
241255
return;
242256
};
243257

244-
for env_file_name in env_file_names.iter() {
245-
match deno_dotenv::from_path(env_file_name) {
246-
Ok(_) => (),
258+
let original_env_keys: HashSet<OsString> =
259+
env::vars_os().map(|(key, _)| key).collect();
260+
let mut loaded_keys = HashSet::new();
261+
262+
for env_file_name in env_file_names.iter().rev() {
263+
let iter = match deno_dotenv::from_path_sanitized_iter(env_file_name) {
264+
Ok(iter) => iter,
247265
Err(error) => {
248266
WatchEnvTracker::handle_dotenv_error(
249267
error,
250268
env_file_name,
251269
flags_log_level,
252270
);
271+
continue;
272+
}
273+
};
274+
275+
for item in iter {
276+
let (key, value) = match item {
277+
Ok(pair) => pair,
278+
Err(error) => {
279+
WatchEnvTracker::handle_dotenv_error(
280+
error,
281+
env_file_name,
282+
flags_log_level,
283+
);
284+
break;
285+
}
286+
};
287+
288+
let key_os = OsString::from(key);
289+
if original_env_keys.contains(&key_os) || loaded_keys.contains(&key_os) {
290+
continue;
291+
}
292+
293+
// SAFETY: We're setting environment variables with sanitized key/value strings from a .env file.
294+
unsafe {
295+
env::set_var(&key_os, value);
253296
}
297+
loaded_keys.insert(key_os);
254298
}
255299
}
256300
}

tests/integration/watcher_tests.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,6 +2237,61 @@ console.log("---");
22372237
check_alive_then_kill(child);
22382238
}
22392239

2240+
#[test(flaky)]
2241+
async fn run_watch_env_file_process_env_precedence() {
2242+
let t = TempDir::new();
2243+
2244+
let env_file = t.path().join(".env");
2245+
std::fs::write(&env_file, "FOO=from_env_file\nBAR=from_env_file").unwrap();
2246+
2247+
let main_script = t.path().join("main.js");
2248+
std::fs::write(
2249+
&main_script,
2250+
r#"
2251+
console.log("FOO:", Deno.env.get("FOO"));
2252+
console.log("BAR:", Deno.env.get("BAR"));
2253+
console.log("---");
2254+
"#,
2255+
)
2256+
.unwrap();
2257+
2258+
let mut child = util::deno_cmd()
2259+
.current_dir(t.path())
2260+
.arg("run")
2261+
.arg("--watch")
2262+
.arg("--allow-env")
2263+
.arg("--env-file=.env")
2264+
.arg("-L")
2265+
.arg("debug")
2266+
.arg(&main_script)
2267+
.env("NO_COLOR", "1")
2268+
.env("FOO", "from_process")
2269+
.piped_output()
2270+
.spawn()
2271+
.unwrap();
2272+
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
2273+
2274+
wait_for_watcher("main.js", &mut stderr_lines).await;
2275+
2276+
wait_contains("FOO: from_process", &mut stdout_lines).await;
2277+
wait_contains("BAR: from_env_file", &mut stdout_lines).await;
2278+
wait_contains("---", &mut stdout_lines).await;
2279+
wait_contains("Process finished", &mut stderr_lines).await;
2280+
2281+
std::fs::write(
2282+
&env_file,
2283+
"FOO=updated_from_env_file\nBAR=updated_from_env_file",
2284+
)
2285+
.unwrap();
2286+
2287+
wait_contains("Restarting", &mut stderr_lines).await;
2288+
wait_contains("FOO: from_process", &mut stdout_lines).await;
2289+
wait_contains("BAR: updated_from_env_file", &mut stdout_lines).await;
2290+
wait_contains("---", &mut stdout_lines).await;
2291+
2292+
check_alive_then_kill(child);
2293+
}
2294+
22402295
#[test(flaky)]
22412296
async fn run_watch_env_file_multiple_files() {
22422297
let t = TempDir::new();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FOO=should_be_overridden_by_process_level_env
2+
BAR=loaded_from_env_file
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"args": "run -A index.ts",
3+
"output": "index.out"
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FOO=overridden_by_process_level_env
2+
BAR=loaded_from_env_file
3+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const cmd = new Deno.Command(Deno.execPath(), {
2+
args: ["run", "--env-file=.env", "-A", "./spawn.ts"],
3+
env: {
4+
FOO: "overridden_by_process_level_env",
5+
},
6+
stdout: "piped",
7+
stderr: "piped",
8+
});
9+
10+
const child = cmd.spawn();
11+
const { code, stderr, stdout } = await child.output();
12+
const decodedStderr = new TextDecoder().decode(stderr);
13+
const decodedStdout = new TextDecoder().decode(stdout);
14+
15+
if (code !== 0) {
16+
console.error("Command failed with code " + code);
17+
console.error("stderr: " + decodedStderr);
18+
Deno.exit(code);
19+
}
20+
21+
console.log(decodedStdout);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log(`FOO=${Deno.env.get("FOO")}`);
2+
console.log(`BAR=${Deno.env.get("BAR")}`);

0 commit comments

Comments
 (0)