diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index 3b343810..84ff854a 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -37,15 +37,25 @@ use users::{ const FORGE_CONTAINER: &str = "hive-forge"; -/// Base HTTP URL for the local Forgejo instance. Reads `HIVE_FORGE_URL` -/// from the environment (set unconditionally by `hive-c0re.nix` to -/// `http://`) so the forge port is never hardcoded. -/// Falls back to `http://localhost:3000` for bare runs outside the -/// NixOS module (tests, manual invocation). +/// Base HTTP URL for the local Forgejo instance, from `HIVE_FORGE_URL` +/// (set unconditionally by `hive-c0re.nix` to `http://`). +/// +/// # Panics +/// +/// When `HIVE_FORGE_URL` is unset. That is deliberate: this daemon only +/// runs under the NixOS module, which always sets it, so an unset var +/// means the deployment is broken. There is no loopback fallback, +/// because a guess is wrong in exactly the cases that matter — the +/// forge may live on a different host from the daemon, and a fallback +/// turns "misconfigured" into "silently talking to the wrong machine" +/// or, worse, "connection refused" surfacing far from its cause. pub(crate) fn forge_http_base() -> &'static str { static BASE: OnceLock = OnceLock::new(); BASE.get_or_init(|| { - std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()) + std::env::var("HIVE_FORGE_URL").expect( + "HIVE_FORGE_URL is unset — hive-c0re.nix sets it unconditionally, \ + so this process was started outside the NixOS module", + ) }) } @@ -53,15 +63,24 @@ pub(crate) fn forge_http_base() -> &'static str { /// `core:` credentials between the scheme and authority of /// [`forge_http_base()`] — the form git accepts for inline auth. pub(crate) fn forge_git_url(token: &str, repo: &str) -> String { - let base = forge_http_base(); - // Split on "://" to isolate scheme + authority. The base URL always - // contains "://" (validated fallback + `HIVE_FORGE_URL` is - // operator-set and expected to be well-formed). - if let Some((scheme, host)) = base.split_once("://") { - format!("{scheme}://core:{token}@{host}/{repo}.git") - } else { - format!("http://core:{token}@localhost:3000/{repo}.git") - } + git_url_with_base(forge_http_base(), token, repo) +} + +/// The credential-insertion half of [`forge_git_url`], split out so it +/// can be tested without a process-wide env var (which would race every +/// other test in this binary). +/// +/// # Panics +/// +/// When `base` has no `://`. Previously this fell back to +/// `http://core:@localhost:3000` — a guess that would have sent +/// a *credentialed* push at whatever answers on the local port. A +/// malformed base is a broken deployment; failing on it is the point. +fn git_url_with_base(base: &str, token: &str, repo: &str) -> String { + let (scheme, host) = base + .split_once("://") + .unwrap_or_else(|| panic!("HIVE_FORGE_URL is not a URL (no \"://\"): {base}")); + format!("{scheme}://core:{token}@{host}/{repo}.git") } /// Forgejo org grouping every agent's config repo. Core is a site admin diff --git a/hive-c0re/src/forge/pr_merge.rs b/hive-c0re/src/forge/pr_merge.rs index 6b685736..758ff514 100644 --- a/hive-c0re/src/forge/pr_merge.rs +++ b/hive-c0re/src/forge/pr_merge.rs @@ -253,7 +253,7 @@ pub async fn post_pr_comment(repo: &str, pr: u64, body: &str) -> Result<(), Forg #[cfg(test)] mod tests { use super::repo_agent_name; - use crate::forge::forge_git_url; + use crate::forge::git_url_with_base; #[test] fn repo_agent_name_takes_trailing_segment() { @@ -264,13 +264,23 @@ mod tests { #[test] fn forge_git_url_shape() { - // Credentials are inserted between scheme and authority; fallback - // base is `http://localhost:3000` when HIVE_FORGE_URL is unset. - let url = forge_git_url("tok", "agent-configs/iris"); - assert!(url.contains("core:tok@"), "must embed credentials: {url}"); + // Tests the pure half: credentials go between scheme and + // authority. Deliberately not via `forge_git_url`, which reads + // HIVE_FORGE_URL — setting that here would race every other + // test in this binary, and there is no fallback to lean on any + // more (a guessed base is the bug this issue removes). + let url = git_url_with_base("http://forge.example.test", "tok", "a/iris"); + assert_eq!(url, "http://core:tok@forge.example.test/a/iris.git"); + } + + #[test] + fn forge_git_url_preserves_https() { + // The scheme is carried through rather than assumed: a swarm + // whose forge is behind TLS must not be downgraded to http. + let url = git_url_with_base("https://forge.example.test", "tok", "a/iris"); assert!( - url.ends_with("/agent-configs/iris.git"), - "must end with repo path: {url}" + url.starts_with("https://core:tok@"), + "https must survive: {url}" ); } }