hive-c0re: validate forge env at startup; char-safe short revs
HIVE_FORGE_URL (set + parseable) and HIVE_ASSETS_DIR are checked once in main before workers start. Short revs in container_view and pr_status take 12 chars instead of byte-slicing.
This commit is contained in:
parent
9b711d5ffa
commit
6bb13f964d
4 changed files with 56 additions and 6 deletions
|
|
@ -187,7 +187,7 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> anyhow::Result<Vec
|
||||||
.map(std::string::String::as_str);
|
.map(std::string::String::as_str);
|
||||||
let needs_update =
|
let needs_update =
|
||||||
crate::auto_update::agent_config_pending(logical.as_str(), deployed_full).await;
|
crate::auto_update::agent_config_pending(logical.as_str(), deployed_full).await;
|
||||||
let deployed_sha = deployed_full.map(|s| s[..s.len().min(12)].to_owned());
|
let deployed_sha = deployed_full.map(short_rev);
|
||||||
// One `systemctl` call for both facts: `unit_state` is the same
|
// One `systemctl` call for both facts: `unit_state` is the same
|
||||||
// shell-out `is_running` makes, minus `--quiet`. Asking twice would
|
// shell-out `is_running` makes, minus `--quiet`. Asking twice would
|
||||||
// double the per-agent subprocess count on every SSE scan.
|
// double the per-agent subprocess count on every SSE scan.
|
||||||
|
|
@ -449,9 +449,28 @@ fn parse_locked_revs(raw: &str) -> HashMap<String, String> {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// First 12 characters of a rev. Counts chars, not bytes: the value comes
|
||||||
|
/// from `flake.lock` JSON, and a byte slice at 12 can split a multi-byte char.
|
||||||
|
fn short_rev(rev: &str) -> String {
|
||||||
|
rev.chars().take(12).collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::parse_locked_revs;
|
use super::{parse_locked_revs, short_rev};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_rev_truncates_and_keeps_short() {
|
||||||
|
assert_eq!(short_rev("0123456789abcdef"), "0123456789ab");
|
||||||
|
assert_eq!(short_rev("abc123"), "abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_rev_survives_multibyte_at_boundary() {
|
||||||
|
// 'é' is 2 bytes and straddles byte index 12.
|
||||||
|
let rev = "0123456789aébcdef";
|
||||||
|
assert_eq!(short_rev(rev), "0123456789aé");
|
||||||
|
}
|
||||||
|
|
||||||
/// Shape of a real `meta/flake.lock`: `root` names a node whose
|
/// Shape of a real `meta/flake.lock`: `root` names a node whose
|
||||||
/// `inputs` map alias → node name, and each node carries `locked.rev`.
|
/// `inputs` map alias → node name, and each node carries `locked.rev`.
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ const FORGE_CONTAINER: &str = "hive-forge";
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// When `HIVE_FORGE_URL` is unset. That is deliberate: this daemon only
|
/// When `HIVE_FORGE_URL` is unset (`validate_env` rejects that at startup, so
|
||||||
|
/// the daemon never reaches this). That is deliberate: this daemon only
|
||||||
/// runs under the NixOS module, which always sets it, so an unset var
|
/// runs under the NixOS module, which always sets it, so an unset var
|
||||||
/// means the deployment is broken. There is no loopback fallback,
|
/// means the deployment is broken. There is no loopback fallback,
|
||||||
/// because a guess is wrong in exactly the cases that matter — the
|
/// because a guess is wrong in exactly the cases that matter — the
|
||||||
|
|
@ -55,6 +56,22 @@ pub(crate) fn forge_http_base() -> &'static str {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check the env the forge integration cannot run without: `HIVE_FORGE_URL`
|
||||||
|
/// set and a valid URL, `HIVE_ASSETS_DIR` set. Called once from `main`
|
||||||
|
/// before any worker is spawned, so a missing or malformed value fails the
|
||||||
|
/// unit start naming the variable; the lazy accessors then never hit their
|
||||||
|
/// `expect`s.
|
||||||
|
pub(crate) fn validate_env() -> Result<()> {
|
||||||
|
let url = std::env::var("HIVE_FORGE_URL").context(
|
||||||
|
"HIVE_FORGE_URL is unset (the hyperhive NixOS module sets it on the hive-c0re unit)",
|
||||||
|
)?;
|
||||||
|
Url::parse(&url).with_context(|| format!("HIVE_FORGE_URL is not a valid URL: {url:?}"))?;
|
||||||
|
std::env::var("HIVE_ASSETS_DIR").context(
|
||||||
|
"HIVE_ASSETS_DIR is unset (the hyperhive NixOS module sets it on the hive-c0re unit)",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Git remote for `repo` (e.g. `"core/meta"`) — **credential-free**.
|
/// Git remote for `repo` (e.g. `"core/meta"`) — **credential-free**.
|
||||||
///
|
///
|
||||||
/// The token does not go here. A URL is a process argument, and `argv` is
|
/// The token does not go here. A URL is a process argument, and `argv` is
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,9 @@ async fn main() -> Result<()> {
|
||||||
// succeeded.
|
// succeeded.
|
||||||
lifecycle::network_isolation_from_env()
|
lifecycle::network_isolation_from_env()
|
||||||
.context("network isolation settings are required at startup")?;
|
.context("network isolation settings are required at startup")?;
|
||||||
|
// The forge integration is mandatory (the NixOS module sets both
|
||||||
|
// vars unconditionally), so check them before any worker starts.
|
||||||
|
forge::validate_env()?;
|
||||||
cmd_serve(sc.env, sc.model_prices, sc.build_slots, db, &cli.socket).await
|
cmd_serve(sc.env, sc.model_prices, sc.build_slots, db, &cli.socket).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,9 +263,15 @@ fn status_mark(status: &str) -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// First 12 characters of a sha. Counts chars, not bytes, so a multi-byte
|
||||||
|
/// char at the cut cannot panic.
|
||||||
|
fn short_sha(sha: &str) -> String {
|
||||||
|
sha.chars().take(12).collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Render the CI section (overall verdict + per-context rows).
|
/// Render the CI section (overall verdict + per-context rows).
|
||||||
fn print_ci(sha: &str, state: &str, statuses: &[Value]) {
|
fn print_ci(sha: &str, state: &str, statuses: &[Value]) {
|
||||||
let short = &sha[..sha.len().min(12)];
|
let short = short_sha(sha);
|
||||||
if statuses.is_empty() {
|
if statuses.is_empty() {
|
||||||
println!("{short}: no CI statuses reported (CI may not have started)");
|
println!("{short}: no CI statuses reported (CI may not have started)");
|
||||||
return;
|
return;
|
||||||
|
|
@ -462,12 +468,17 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn short_sha_truncates() {
|
fn short_sha_truncates() {
|
||||||
let sha = "0123456789abcdef0123456789abcdef";
|
let sha = "0123456789abcdef0123456789abcdef";
|
||||||
assert_eq!(&sha[..sha.len().min(12)], "0123456789ab");
|
assert_eq!(short_sha(sha), "0123456789ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn short_sha_handles_already_short() {
|
fn short_sha_handles_already_short() {
|
||||||
assert_eq!(&"abc123"[.."abc123".len().min(12)], "abc123");
|
assert_eq!(short_sha("abc123"), "abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_sha_survives_multibyte_at_boundary() {
|
||||||
|
assert_eq!(short_sha("0123456789aébcdef"), "0123456789aé");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue