max / makenotwork
8 files changed,
+49 insertions,
-56 deletions
| @@ -4,8 +4,7 @@ | |||
| 4 | 4 | //! - `file:///abs/path/to/dump.sql.gz` — local copy (dev). | |
| 5 | 5 | //! - `rsync://host/module/path` — rsync daemon protocol. | |
| 6 | 6 | //! - `ssh://user@host[:port]/path/file.sql.gz` — rsync-over-ssh. Used to pull | |
| 7 | - | //! prod backups from | |
| 8 | - | //! `backup-puller@alpha-west-1`. | |
| 7 | + | //! prod backups from `backup-puller@alpha-west-1`. | |
| 9 | 8 | //! | |
| 10 | 9 | //! The fetch is command-driven: the operator triggers it via /backup/fetch, it | |
| 11 | 10 | //! is not implicit in promote. That keeps the slowest, most failure-prone step |
| @@ -45,11 +45,10 @@ pub async fn run( | |||
| 45 | 45 | // deploy branch so a just-pushed sha is locally resolvable. A fetch | |
| 46 | 46 | // failure is non-fatal — the sha may already be present from a prior | |
| 47 | 47 | // fetch or a direct push; the presence check below is the real gate. | |
| 48 | - | if let Some(upstream) = topo.repo.upstream.as_deref() { | |
| 49 | - | if let Err(e) = git::fetch_upstream(&bare, upstream, &topo.repo.branch).await { | |
| 48 | + | if let Some(upstream) = topo.repo.upstream.as_deref() | |
| 49 | + | && let Err(e) = git::fetch_upstream(&bare, upstream, &topo.repo.branch).await { | |
| 50 | 50 | tracing::warn!(error = %e, upstream, "upstream fetch failed; proceeding with current bare-repo state"); | |
| 51 | 51 | } | |
| 52 | - | } | |
| 53 | 52 | anyhow::ensure!( | |
| 54 | 53 | git::sha_present(&bare, sha.as_str()).await?, | |
| 55 | 54 | "sha {} not present in bare repo {} after fetch — push the commit to the upstream remote first", |
| @@ -32,11 +32,10 @@ pub fn classify_cargo_test(stdout: &[u8], stderr: &[u8]) -> GateFailure { | |||
| 32 | 32 | // Expect "P passed; F failed; ..." | |
| 33 | 33 | for piece in rest.split(';') { | |
| 34 | 34 | let p = piece.trim(); | |
| 35 | - | if let Some(num_str) = p.strip_suffix(" failed") { | |
| 36 | - | if let Ok(n) = num_str.parse::<u32>() { | |
| 35 | + | if let Some(num_str) = p.strip_suffix(" failed") | |
| 36 | + | && let Ok(n) = num_str.parse::<u32>() { | |
| 37 | 37 | failed_count = n; | |
| 38 | 38 | } | |
| 39 | - | } | |
| 40 | 39 | } | |
| 41 | 40 | break; | |
| 42 | 41 | } | |
| @@ -45,14 +44,13 @@ pub fn classify_cargo_test(stdout: &[u8], stderr: &[u8]) -> GateFailure { | |||
| 45 | 44 | // libtest prints "failures:\n foo::bar" near the end too. Grab | |
| 46 | 45 | // the first one for the summary line. | |
| 47 | 46 | if let Some(idx) = stdout_s.find("\nfailures:\n") { | |
| 48 | - | for line in stdout_s[idx + 11..].lines() { | |
| 47 | + | // The "failures:" block repeats — once with stdout per failure, once as | |
| 48 | + | // a plain name list. Either way the first non-empty line is a candidate. | |
| 49 | + | if let Some(line) = stdout_s[idx + 11..].lines().next() { | |
| 49 | 50 | let trimmed = line.trim(); | |
| 50 | - | if trimmed.is_empty() { break; } | |
| 51 | - | // The "failures:" block repeats — once with stdout per | |
| 52 | - | // failure, once as a plain name list. Either way the first | |
| 53 | - | // non-empty line is a candidate. | |
| 54 | - | first_failed = Some(trimmed.to_string()); | |
| 55 | - | break; | |
| 51 | + | if !trimmed.is_empty() { | |
| 52 | + | first_failed = Some(trimmed.to_string()); | |
| 53 | + | } | |
| 56 | 54 | } | |
| 57 | 55 | } | |
| 58 | 56 | ||
| @@ -286,7 +284,7 @@ fn combined_tail_for_classifier(stdout: &[u8], stderr: &[u8]) -> String { | |||
| 286 | 284 | let mut joined = Vec::with_capacity(stdout.len() + stderr.len() + 32); | |
| 287 | 285 | joined.extend_from_slice(b"==== stdout ====\n"); | |
| 288 | 286 | joined.extend_from_slice(stdout); | |
| 289 | - | if !stdout.last().is_some_and(|b| *b == b'\n') { joined.push(b'\n'); } | |
| 287 | + | if stdout.last().is_none_or(|b| *b != b'\n') { joined.push(b'\n'); } | |
| 290 | 288 | joined.extend_from_slice(b"==== stderr ====\n"); | |
| 291 | 289 | joined.extend_from_slice(stderr); | |
| 292 | 290 | let s = String::from_utf8_lossy(&joined); |
| @@ -64,32 +64,6 @@ pub struct ReleaseEntry { | |||
| 64 | 64 | fn default_bin_names() -> Vec<String> { vec!["server".into()] } | |
| 65 | 65 | fn default_logs_root() -> PathBuf { PathBuf::from("/srv/sando/logs") } | |
| 66 | 66 | ||
| 67 | - | #[cfg(test)] | |
| 68 | - | mod tests { | |
| 69 | - | use super::*; | |
| 70 | - | ||
| 71 | - | const MINIMAL: &str = r#" | |
| 72 | - | listen = "127.0.0.1:7766" | |
| 73 | - | db_path = "./sando.db" | |
| 74 | - | topology_path = "../sando.toml" | |
| 75 | - | workdir = "./work" | |
| 76 | - | release_root = "./releases" | |
| 77 | - | "#; | |
| 78 | - | ||
| 79 | - | #[test] | |
| 80 | - | fn cargo_target_dir_parses_when_present() { | |
| 81 | - | let raw = format!("{MINIMAL}\ncargo_target_dir = \"/srv/sando/cargo-target\"\n"); | |
| 82 | - | let cfg: Config = toml::from_str(&raw).unwrap(); | |
| 83 | - | assert_eq!(cfg.cargo_target_dir.as_deref(), Some(std::path::Path::new("/srv/sando/cargo-target"))); | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | #[test] | |
| 87 | - | fn cargo_target_dir_defaults_to_none() { | |
| 88 | - | let cfg: Config = toml::from_str(MINIMAL).unwrap(); | |
| 89 | - | assert!(cfg.cargo_target_dir.is_none(), "omitting it keeps the per-worktree target/"); | |
| 90 | - | } | |
| 91 | - | } | |
| 92 | - | ||
| 93 | 67 | impl Config { | |
| 94 | 68 | /// Primary binary — the one the systemd unit's ExecStart points at. | |
| 95 | 69 | pub fn primary_bin(&self) -> &str { | |
| @@ -119,3 +93,29 @@ impl Config { | |||
| 119 | 93 | } | |
| 120 | 94 | } | |
| 121 | 95 | } | |
| 96 | + | ||
| 97 | + | #[cfg(test)] | |
| 98 | + | mod tests { | |
| 99 | + | use super::*; | |
| 100 | + | ||
| 101 | + | const MINIMAL: &str = r#" | |
| 102 | + | listen = "127.0.0.1:7766" | |
| 103 | + | db_path = "./sando.db" | |
| 104 | + | topology_path = "../sando.toml" | |
| 105 | + | workdir = "./work" | |
| 106 | + | release_root = "./releases" | |
| 107 | + | "#; | |
| 108 | + | ||
| 109 | + | #[test] | |
| 110 | + | fn cargo_target_dir_parses_when_present() { | |
| 111 | + | let raw = format!("{MINIMAL}\ncargo_target_dir = \"/srv/sando/cargo-target\"\n"); | |
| 112 | + | let cfg: Config = toml::from_str(&raw).unwrap(); | |
| 113 | + | assert_eq!(cfg.cargo_target_dir.as_deref(), Some(std::path::Path::new("/srv/sando/cargo-target"))); | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | #[test] | |
| 117 | + | fn cargo_target_dir_defaults_to_none() { | |
| 118 | + | let cfg: Config = toml::from_str(MINIMAL).unwrap(); | |
| 119 | + | assert!(cfg.cargo_target_dir.is_none(), "omitting it keeps the per-worktree target/"); | |
| 120 | + | } | |
| 121 | + | } |
| @@ -231,7 +231,7 @@ async fn gc_local_releases(release_root: &Path) -> Result<()> { | |||
| 231 | 231 | let meta = entry.metadata().await?; | |
| 232 | 232 | entries.push((entry.path(), meta.modified()?)); | |
| 233 | 233 | } | |
| 234 | - | entries.sort_by(|a, b| b.1.cmp(&a.1)); | |
| 234 | + | entries.sort_by_key(|e| std::cmp::Reverse(e.1)); | |
| 235 | 235 | for (path, _) in entries.into_iter().skip(RELEASES_TO_KEEP) { | |
| 236 | 236 | if let Err(e) = tokio::fs::remove_dir_all(&path).await { | |
| 237 | 237 | tracing::warn!(path = %path.display(), error = %e, "gc: rm failed"); | |
| @@ -311,9 +311,9 @@ mod tests { | |||
| 311 | 311 | let release_root = root.join("rr"); | |
| 312 | 312 | tokio::fs::create_dir_all(&release_root).await.unwrap(); | |
| 313 | 313 | ||
| 314 | - | deploy_local(&release_root, &"0.1.0".parse().unwrap(), &[bin.clone()]).await.unwrap(); | |
| 314 | + | deploy_local(&release_root, &"0.1.0".parse().unwrap(), std::slice::from_ref(&bin)).await.unwrap(); | |
| 315 | 315 | tokio::fs::write(&bin, b"V2").await.unwrap(); | |
| 316 | - | deploy_local(&release_root, &"0.2.0".parse().unwrap(), &[bin.clone()]).await.unwrap(); | |
| 316 | + | deploy_local(&release_root, &"0.2.0".parse().unwrap(), std::slice::from_ref(&bin)).await.unwrap(); | |
| 317 | 317 | ||
| 318 | 318 | assert!(release_root.join("releases/0.1.0/server").exists()); | |
| 319 | 319 | assert!(release_root.join("releases/0.2.0/server").exists()); |
| @@ -717,7 +717,7 @@ async fn persist_gate_log(ctx: &GateCtx, gate_kind: GateKind, stdout: &[u8], std | |||
| 717 | 717 | let mut body = Vec::with_capacity(stdout.len() + stderr.len() + 64); | |
| 718 | 718 | body.extend_from_slice(b"==== stdout ====\n"); | |
| 719 | 719 | body.extend_from_slice(stdout); | |
| 720 | - | if !stdout.last().is_some_and(|b| *b == b'\n') { | |
| 720 | + | if stdout.last().is_none_or(|b| *b != b'\n') { | |
| 721 | 721 | body.push(b'\n'); | |
| 722 | 722 | } | |
| 723 | 723 | body.extend_from_slice(b"==== stderr ====\n"); |
| @@ -45,12 +45,11 @@ impl LiveLog { | |||
| 45 | 45 | /// watching live still see the chunk. | |
| 46 | 46 | pub async fn write_chunk(&mut self, bytes: &[u8]) { | |
| 47 | 47 | if bytes.is_empty() { return; } | |
| 48 | - | if let Some(f) = self.file.as_mut() { | |
| 49 | - | if let Err(e) = f.write_all(bytes).await { | |
| 48 | + | if let Some(f) = self.file.as_mut() | |
| 49 | + | && let Err(e) = f.write_all(bytes).await { | |
| 50 | 50 | tracing::warn!(error = %e, path = %self.path.display(), "live log write failed"); | |
| 51 | 51 | self.file = None; | |
| 52 | 52 | } | |
| 53 | - | } | |
| 54 | 53 | let text = String::from_utf8_lossy(bytes).into_owned(); | |
| 55 | 54 | events::emit(&self.events, Event::GateLogChunk { | |
| 56 | 55 | run_id: self.run_id, | |
| @@ -62,11 +61,10 @@ impl LiveLog { | |||
| 62 | 61 | ||
| 63 | 62 | /// Flush and close the file. Best-effort: errors are logged. | |
| 64 | 63 | pub async fn close(mut self) { | |
| 65 | - | if let Some(mut f) = self.file.take() { | |
| 66 | - | if let Err(e) = f.flush().await { | |
| 64 | + | if let Some(mut f) = self.file.take() | |
| 65 | + | && let Err(e) = f.flush().await { | |
| 67 | 66 | tracing::warn!(error = %e, path = %self.path.display(), "live log flush failed"); | |
| 68 | 67 | } | |
| 69 | - | } | |
| 70 | 68 | } | |
| 71 | 69 | ||
| 72 | 70 | pub fn run_id(&self) -> GateRunId { self.run_id } | |
| @@ -86,12 +84,11 @@ impl ops_exec::LogSink for LiveLog { | |||
| 86 | 84 | } | |
| 87 | 85 | ||
| 88 | 86 | async fn open_for_append(path: &Path) -> Option<File> { | |
| 89 | - | if let Some(parent) = path.parent() { | |
| 90 | - | if let Err(e) = tokio::fs::create_dir_all(parent).await { | |
| 87 | + | if let Some(parent) = path.parent() | |
| 88 | + | && let Err(e) = tokio::fs::create_dir_all(parent).await { | |
| 91 | 89 | tracing::warn!(error = %e, dir = %parent.display(), "could not create gate log dir"); | |
| 92 | 90 | return None; | |
| 93 | 91 | } | |
| 94 | - | } | |
| 95 | 92 | match tokio::fs::OpenOptions::new() | |
| 96 | 93 | .create(true) | |
| 97 | 94 | .append(true) |
| @@ -27,7 +27,7 @@ async fn main() -> Result<()> { | |||
| 27 | 27 | git::ensure_bare_repo(Path::new(&topo.repo.bare_path)).await?; | |
| 28 | 28 | let pool = db::connect(&cfg.db_path).await?; | |
| 29 | 29 | db::migrate(&pool).await?; | |
| 30 | - | sync::sync(&pool, &*topo).await?; | |
| 30 | + | sync::sync(&pool, &topo).await?; | |
| 31 | 31 | tracing::info!(tiers = topo.tiers.len(), bare = %topo.repo.bare_path, "topology synced"); | |
| 32 | 32 | ||
| 33 | 33 | let prom = metrics::init(); |