Skip to content
Merged
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
59 changes: 59 additions & 0 deletions crates/sprout-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,19 @@ impl Config {
let git_repo_path: std::path::PathBuf = std::env::var("SPROUT_GIT_REPO_PATH")
.unwrap_or_else(|_| "./repos".to_string())
.into();
// Ensure the git repo root exists. The smart-HTTP transport and the
// kind:30617 side-effect handler both canonicalize this path; if it's
// missing, all git operations 500 with "git service misconfigured" and
// repo announcements silently fail to create their bare repo on disk.
// Bootstrapping here makes the relay self-provision its own data dir
// (matches how we treat other relay-owned paths) rather than requiring
// ops to mkdir it out of band.
if let Err(e) = std::fs::create_dir_all(&git_repo_path) {
return Err(ConfigError::InvalidValue(format!(
"SPROUT_GIT_REPO_PATH={} could not be created: {e}",
git_repo_path.display()
)));
}
let git_max_pack_bytes: u64 = std::env::var("SPROUT_GIT_MAX_PACK_BYTES")
.ok()
.and_then(|v| v.parse().ok())
Expand Down Expand Up @@ -440,6 +453,52 @@ mod tests {
);
}

#[test]
fn git_repo_path_is_created_if_missing() {
let _guard = ENV_MUTEX.lock().unwrap();
// Pick a path under temp_dir that definitely doesn't exist yet.
let base = std::env::temp_dir().join(format!(
"sprout-test-git-repo-path-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let nested = base.join("nested").join("repos");
assert!(!nested.exists(), "test precondition: path must not exist");

std::env::set_var("SPROUT_GIT_REPO_PATH", &nested);
let result = Config::from_env();
std::env::remove_var("SPROUT_GIT_REPO_PATH");

let config = result.expect("config should self-bootstrap missing git_repo_path");
assert_eq!(config.git_repo_path, nested);
assert!(
nested.is_dir(),
"git_repo_path should exist after config load"
);

// Cleanup.
let _ = std::fs::remove_dir_all(&base);
}

#[test]
#[cfg(unix)]
fn git_repo_path_unwritable_returns_error() {
let _guard = ENV_MUTEX.lock().unwrap();
// Try to create a path under a regular file — must fail.
// Using /dev/null as the parent guarantees create_dir_all fails on unix.
let bogus = std::path::PathBuf::from("/dev/null/cannot-create-here");
std::env::set_var("SPROUT_GIT_REPO_PATH", &bogus);
let result = Config::from_env();
std::env::remove_var("SPROUT_GIT_REPO_PATH");
assert!(
matches!(result, Err(ConfigError::InvalidValue(ref msg)) if msg.contains("SPROUT_GIT_REPO_PATH")),
"expected InvalidValue mentioning SPROUT_GIT_REPO_PATH, got {result:?}"
);
}

#[test]
fn server_domain_explicit_override_wins() {
let _guard = ENV_MUTEX.lock().unwrap();
Expand Down
14 changes: 14 additions & 0 deletions crates/sprout-relay/src/handlers/side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,20 @@ async fn handle_git_repo_announcement(event: &Event, state: &Arc<AppState>) -> a

// Resolve repo path.
let git_repo_root = &state.config.git_repo_path;

// Defensive: ensure the configured root exists. Config bootstrap creates
// this at startup, but a misconfigured deployment or out-of-band deletion
// would otherwise cause every canonicalize() below to fail and the
// side-effect to be silently swallowed by the ingest pipeline, leaving the
// repo announcement stored but no bare repo on disk (push then 500s with
// "git service misconfigured").
if let Err(e) = std::fs::create_dir_all(git_repo_root) {
return Err(anyhow::anyhow!(
"failed to ensure git_repo_path {} exists: {e}",
git_repo_root.display()
));
}

let repo_dir = git_repo_root
.join(&owner_hex)
.join(format!("{repo_id}.git"));
Expand Down
Loading