Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,14 @@ full cold-read/resource acceptance gates listed in `docs/spec/README.md`.
and candidate external-boundary proof remain separate obligations; local loose objects and retired
download membership cannot justify retirement. See the cost and remaining-evidence rows in the linked docs.

- **D50 (2026-10-01): `WALGIT__` overrides fail closed.** An environment override this build cannot
apply (malformed name, unknown section or key, wrong type) is a startup and `config check` error that
names each variable and the nearest known key; nothing of the batch is applied. This supersedes the
warn-and-ignore loader (2026-08-21: a key newer than one host's image crash-looped it): a host running
on a default it was told to change is the worse failure, and the file already refuses unknown keys.
Roll the image before the env that needs it; gate the rollout on `walgit config check --env-file`
run by the binary that will serve.

## 5. Working rules

- **No backwards compatibility (pre-1.0, banner at top):** change the shape and delete the old one in the same
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 3 additions & 16 deletions crates/walgit-cli/src/config_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use walgit_config::Config;

pub async fn run(action: ConfigAction, cfg: &Arc<Config>) -> Result<()> {
match action {
ConfigAction::Check { env_files, strict } => {
ConfigAction::Check { env_files } => {
let mut cfg: Config = (**cfg).clone();
let mut vars: Vec<(String, String)> = Vec::new();
for f in &env_files {
Expand All @@ -25,22 +25,9 @@ pub async fn run(action: ConfigAction, cfg: &Arc<Config>) -> Result<()> {
}
}
}
let ignored = cfg.apply_env_report(vars.into_iter())?;
cfg.apply_env(vars.into_iter())?;
cfg.validate()?;
for (k, why) in &ignored {
eprintln!("ignored {k}: {why}");
}
if ignored.is_empty() {
println!("config OK");
} else {
println!(
"config OK ({} override(s) ignored — unknown in this build)",
ignored.len()
);
if strict {
std::process::exit(3);
}
}
println!("config OK");
Ok(())
}
ConfigAction::Dump => {
Expand Down
7 changes: 2 additions & 5 deletions crates/walgit-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,15 +376,12 @@ enum ConfigAction {
/// Parse and validate the config file; print OK or the error.
Check {
/// Also apply `WALGIT__*` overrides from these env files (KEY=VALUE
/// lines; `#` comments) on top of the process env, report every key
/// this build ignores, and exit 3 when `--strict` and any was ignored.
/// lines; `#` comments) on top of the process env; an override this
/// build cannot apply fails the check exactly as it fails startup.
/// Useful in a process supervisor's pre-start check to validate an env
/// file against the binary that will actually run.
#[arg(long = "env-file")]
env_files: Vec<std::path::PathBuf>,
/// Exit 3 if any override was ignored (unknown in this build).
#[arg(long)]
strict: bool,
},
/// Print the effective config as TOML.
Dump,
Expand Down
94 changes: 94 additions & 0 deletions crates/walgit-cli/tests/config_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Test fixtures use panics to fail the test.
#![allow(clippy::unwrap_used)]
//! `WALGIT__` overrides fail closed through the real binaries: a typo'd
//! override stops `config check` and server startup (before anything binds),
//! naming the variable; a valid one is applied.

use std::process::{Command, Output};

const TYPO: (&str, &str) = ("WALGIT__SERVER__LSITEN", "127.0.0.1:0");
const HINT: &str = "WALGIT__SERVER__LSITEN: unknown key `lsiten` in [server]; did you mean WALGIT__SERVER__LISTEN?";

/// A binary with `--config /dev/null` (defaults + env, D39) and only `env`:
/// nothing from the test runner's environment leaks in.
fn run(bin: &str, args: &[&str], env: &[(&str, &str)]) -> Output {
let mut cmd = Command::new(bin);
cmd.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.args(["--config", "/dev/null"])
.args(args);
for (k, v) in env {
cmd.env(k, v);
}
cmd.output().unwrap()
}

fn stderr(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}

#[test]
fn server_startup_refuses_a_typod_override() {
// Spawned, not `output()`: a server that ignored the typo would serve forever.
let mut child = Command::new(env!("CARGO_BIN_EXE_walgit-server"))
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env(TYPO.0, TYPO.1)
.args(["--config", "/dev/null"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_mins(1);
while child.try_wait().unwrap().is_none() {
if std::time::Instant::now() > deadline {
child.kill().unwrap();
panic!("walgit-server started with {} set", TYPO.0);
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let out = child.wait_with_output().unwrap();
assert_eq!(out.status.code(), Some(1), "{}", stderr(&out));
assert!(stderr(&out).contains(HINT), "{}", stderr(&out));
}

#[test]
fn config_check_refuses_a_typod_override_in_the_env_and_in_an_env_file() {
let walgit = env!("CARGO_BIN_EXE_walgit");
let out = run(walgit, &["config", "check"], &[TYPO]);
assert!(!out.status.success(), "process env typo accepted");
assert!(stderr(&out).contains(HINT), "{}", stderr(&out));

let dir = tempfile::tempdir().unwrap();
let env_file = dir.path().join("walgit.env");
std::fs::write(
&env_file,
format!(
"# host overrides\nWALGIT__WAL__MAX_BATCH=7\n{}={}\n",
TYPO.0, TYPO.1
),
)
.unwrap();
let path = env_file.to_str().unwrap();
let out = run(walgit, &["config", "check", "--env-file", path], &[]);
assert!(!out.status.success(), "env-file typo accepted");
assert!(stderr(&out).contains(HINT), "{}", stderr(&out));
assert!(!String::from_utf8_lossy(&out.stdout).contains("config OK"));

std::fs::write(&env_file, "WALGIT__WAL__MAX_BATCH=7\n").unwrap();
let out = run(walgit, &["config", "check", "--env-file", path], &[]);
assert!(out.status.success(), "{}", stderr(&out));
assert!(String::from_utf8_lossy(&out.stdout).contains("config OK"));
}

#[test]
fn a_valid_override_is_applied() {
let out = run(
env!("CARGO_BIN_EXE_walgit"),
&["config", "dump"],
&[("WALGIT__WAL__MAX_BATCH", "7")],
);
assert!(out.status.success(), "{}", stderr(&out));
let dump = String::from_utf8_lossy(&out.stdout);
assert!(dump.contains("max_batch = 7"), "{dump}");
}
1 change: 0 additions & 1 deletion crates/walgit-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
sha2.workspace = true
hex.workspace = true
tracing.workspace = true
anyhow.workspace = true
serde.workspace = true
toml.workspace = true
Expand Down
Loading