use std::collections::BTreeMap;
use std::process::Command;
use mira::run::{Environment, GitInfo};
const CI_LABELS: &[(&str, &str)] = &[
("ci", "CI"),
("ci.provider", "CI_PROVIDER"),
("ci.workflow", "GITHUB_WORKFLOW"),
("ci.run_id", "GITHUB_RUN_ID"),
("ci.run_attempt", "GITHUB_RUN_ATTEMPT"),
("ci.actor", "GITHUB_ACTOR"),
("ci.pipeline_id", "CI_PIPELINE_ID"),
("ci.job_url", "CI_JOB_URL"),
];
pub fn collect(extra_labels: &BTreeMap<String, String>) -> Option<Environment> {
let mut env = Environment {
git: git_info(),
os: non_empty(std::env::consts::OS),
arch: non_empty(std::env::consts::ARCH),
hostname: hostname(),
cpus: std::thread::available_parallelism().ok().map(|n| n.get()),
mem_total_mib: mem_total_mib(),
mira_version: non_empty(env!("CARGO_PKG_VERSION")),
labels: BTreeMap::new(),
};
for (key, var) in CI_LABELS {
if let Ok(val) = std::env::var(var)
&& !val.is_empty()
{
env.labels.insert((*key).to_string(), val);
}
}
for (k, v) in extra_labels {
env.labels.insert(k.clone(), v.clone());
}
if env.is_empty() { None } else { Some(env) }
}
fn non_empty(s: &str) -> Option<String> {
(!s.is_empty()).then(|| s.to_string())
}
fn git_info() -> Option<GitInfo> {
let commit = git(&["rev-parse", "HEAD"])?;
let branch = git(&["rev-parse", "--abbrev-ref", "HEAD"]).filter(|b| b != "HEAD");
let dirty = git(&["status", "--porcelain"])
.map(|s| !s.is_empty())
.unwrap_or(false);
Some(GitInfo {
commit,
branch,
dirty,
})
}
fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!s.is_empty()).then_some(s)
}
fn hostname() -> Option<String> {
if let Ok(h) = std::env::var("HOSTNAME").or_else(|_| std::env::var("COMPUTERNAME"))
&& !h.is_empty()
{
return Some(h);
}
let out = Command::new("hostname").output().ok()?;
if !out.status.success() {
return None;
}
let h = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!h.is_empty()).then_some(h)
}
fn mem_total_mib() -> Option<u64> {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
let line = text.lines().find(|l| l.starts_with("MemTotal:"))?;
let kb: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
Some(kb / 1024)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_in_repo_has_git_and_box() {
let env = collect(&BTreeMap::new()).expect("something is always captured");
assert!(env.os.is_some());
assert!(env.arch.is_some());
assert!(env.cpus.unwrap() >= 1);
assert!(env.mira_version.is_some());
let git = env.git.expect("captured in a git checkout");
assert_eq!(git.commit.len(), 40, "full sha: {}", git.commit);
}
#[test]
fn config_labels_win_over_detected() {
let mut labels = BTreeMap::new();
labels.insert("team".to_string(), "search".to_string());
let env = collect(&labels).unwrap();
assert_eq!(env.labels.get("team").map(String::as_str), Some("search"));
}
}