Skip to content

Commit 2bcb110

Browse files
author
npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6
committed
fix(relay): auto-create git_repo_path so kind:30617 init can't silently fail
The git smart-HTTP transport and the kind:30617 side-effect handler both canonicalize `config.git_repo_path` before any disk work. If that directory doesn't exist on the relay host: - The transport returns HTTP 500 'git service misconfigured' (transport.rs:237) to every clone/push. - The side-effect handler that initializes the bare repo from a repo announcement fails at canonicalize, and the ingest pipeline only logs side-effect failures at warn! level (ingest.rs:1530) — so the announcement event is stored but no repo is ever created on disk. The relay looks like it accepted the repo creation, but pushes 500 forever. This previously required operators to mkdir the directory out of band. Since the relay already owns the data layout below that root (`{owner}/{repo}.git`, `.names/`), it should self-provision the root the same way. Changes: - `config.rs`: `create_dir_all(git_repo_path)` during config load. Fails loudly with ConfigError::InvalidValue if the path can't be created (e.g. permissions, path-under-file), instead of letting the relay come up healthy with a broken git service. - `side_effects.rs::handle_git_repo_announcement`: defensive `create_dir_all(git_repo_root)` before canonicalize, in case the directory is removed at runtime or the deployment skipped config bootstrap somehow. Returns a clear error rather than the cryptic 'failed to canonicalize repo root' from a missing dir. Tests: - `git_repo_path_is_created_if_missing`: config load against a non-existent nested path succeeds and creates the directory. - `git_repo_path_unwritable_returns_error` (unix): config load against a path under `/dev/null` returns ConfigError::InvalidValue. Post-deploy note: existing kind:30617 announcements published against a broken-root relay won't auto-heal — their side effect already ran (and was swallowed). Owners need to re-publish the announcement to trigger init.
1 parent 6b4f9f0 commit 2bcb110

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

crates/sprout-relay/src/config.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ impl Config {
287287
let git_repo_path: std::path::PathBuf = std::env::var("SPROUT_GIT_REPO_PATH")
288288
.unwrap_or_else(|_| "./repos".to_string())
289289
.into();
290+
// Ensure the git repo root exists. The smart-HTTP transport and the
291+
// kind:30617 side-effect handler both canonicalize this path; if it's
292+
// missing, all git operations 500 with "git service misconfigured" and
293+
// repo announcements silently fail to create their bare repo on disk.
294+
// Bootstrapping here makes the relay self-provision its own data dir
295+
// (matches how we treat other relay-owned paths) rather than requiring
296+
// ops to mkdir it out of band.
297+
if let Err(e) = std::fs::create_dir_all(&git_repo_path) {
298+
return Err(ConfigError::InvalidValue(format!(
299+
"SPROUT_GIT_REPO_PATH={} could not be created: {e}",
300+
git_repo_path.display()
301+
)));
302+
}
290303
let git_max_pack_bytes: u64 = std::env::var("SPROUT_GIT_MAX_PACK_BYTES")
291304
.ok()
292305
.and_then(|v| v.parse().ok())
@@ -440,6 +453,52 @@ mod tests {
440453
);
441454
}
442455

456+
#[test]
457+
fn git_repo_path_is_created_if_missing() {
458+
let _guard = ENV_MUTEX.lock().unwrap();
459+
// Pick a path under temp_dir that definitely doesn't exist yet.
460+
let base = std::env::temp_dir().join(format!(
461+
"sprout-test-git-repo-path-{}-{}",
462+
std::process::id(),
463+
std::time::SystemTime::now()
464+
.duration_since(std::time::UNIX_EPOCH)
465+
.unwrap()
466+
.as_nanos()
467+
));
468+
let nested = base.join("nested").join("repos");
469+
assert!(!nested.exists(), "test precondition: path must not exist");
470+
471+
std::env::set_var("SPROUT_GIT_REPO_PATH", &nested);
472+
let result = Config::from_env();
473+
std::env::remove_var("SPROUT_GIT_REPO_PATH");
474+
475+
let config = result.expect("config should self-bootstrap missing git_repo_path");
476+
assert_eq!(config.git_repo_path, nested);
477+
assert!(
478+
nested.is_dir(),
479+
"git_repo_path should exist after config load"
480+
);
481+
482+
// Cleanup.
483+
let _ = std::fs::remove_dir_all(&base);
484+
}
485+
486+
#[test]
487+
#[cfg(unix)]
488+
fn git_repo_path_unwritable_returns_error() {
489+
let _guard = ENV_MUTEX.lock().unwrap();
490+
// Try to create a path under a regular file — must fail.
491+
// Using /dev/null as the parent guarantees create_dir_all fails on unix.
492+
let bogus = std::path::PathBuf::from("/dev/null/cannot-create-here");
493+
std::env::set_var("SPROUT_GIT_REPO_PATH", &bogus);
494+
let result = Config::from_env();
495+
std::env::remove_var("SPROUT_GIT_REPO_PATH");
496+
assert!(
497+
matches!(result, Err(ConfigError::InvalidValue(ref msg)) if msg.contains("SPROUT_GIT_REPO_PATH")),
498+
"expected InvalidValue mentioning SPROUT_GIT_REPO_PATH, got {result:?}"
499+
);
500+
}
501+
443502
#[test]
444503
fn server_domain_explicit_override_wins() {
445504
let _guard = ENV_MUTEX.lock().unwrap();

crates/sprout-relay/src/handlers/side_effects.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,20 @@ async fn handle_git_repo_announcement(event: &Event, state: &Arc<AppState>) -> a
16201620

16211621
// Resolve repo path.
16221622
let git_repo_root = &state.config.git_repo_path;
1623+
1624+
// Defensive: ensure the configured root exists. Config bootstrap creates
1625+
// this at startup, but a misconfigured deployment or out-of-band deletion
1626+
// would otherwise cause every canonicalize() below to fail and the
1627+
// side-effect to be silently swallowed by the ingest pipeline, leaving the
1628+
// repo announcement stored but no bare repo on disk (push then 500s with
1629+
// "git service misconfigured").
1630+
if let Err(e) = std::fs::create_dir_all(git_repo_root) {
1631+
return Err(anyhow::anyhow!(
1632+
"failed to ensure git_repo_path {} exists: {e}",
1633+
git_repo_root.display()
1634+
));
1635+
}
1636+
16231637
let repo_dir = git_repo_root
16241638
.join(&owner_hex)
16251639
.join(format!("{repo_id}.git"));

0 commit comments

Comments
 (0)