use jiff::Timestamp;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RunContext {
pub observation: Timestamp,
pub git: GitInfo,
pub env: EnvironmentInfo,
pub toolchain: ToolchainInfo,
pub tool_version: String,
}
impl RunContext {
#[must_use]
pub fn new(
observation: Timestamp,
git: GitInfo,
env: EnvironmentInfo,
toolchain: ToolchainInfo,
tool_version: String,
) -> Self {
Self {
observation,
git,
env,
toolchain,
tool_version,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct GitInfo {
pub commit: Option<String>,
pub branch: Option<String>,
pub dirty: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct EnvironmentInfo {
pub provider: EnvironmentProvider,
pub run_id: Option<String>,
pub pull_request: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EnvironmentProvider {
#[default]
Local,
#[serde(rename = "github_actions")]
GitHubActions,
#[serde(rename = "azure_devops")]
AzureDevOps,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ToolchainInfo {
pub target_triple: String,
pub rustc_version: Option<String>,
}
#[must_use]
pub fn detect_environment(get: impl Fn(&str) -> Option<String>) -> EnvironmentInfo {
if get("GITHUB_ACTIONS").as_deref() == Some("true") {
return EnvironmentInfo {
provider: EnvironmentProvider::GitHubActions,
run_id: get("GITHUB_RUN_ID"),
pull_request: None,
};
}
if get("TF_BUILD").is_some() {
return EnvironmentInfo {
provider: EnvironmentProvider::AzureDevOps,
run_id: get("BUILD_BUILDID"),
pull_request: get("SYSTEM_PULLREQUEST_PULLREQUESTID"),
};
}
EnvironmentInfo::default()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::collections::HashMap;
use super::*;
fn env_from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let map: HashMap<String, String> = pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect();
move |name: &str| map.get(name).cloned()
}
#[test]
fn detect_environment_recognizes_github_actions() {
let env = env_from(&[("GITHUB_ACTIONS", "true"), ("GITHUB_RUN_ID", "42")]);
let detected = detect_environment(env);
assert_eq!(detected.provider, EnvironmentProvider::GitHubActions);
assert_eq!(detected.run_id.as_deref(), Some("42"));
}
#[test]
fn detect_environment_recognizes_azure_devops() {
let env = env_from(&[
("TF_BUILD", "True"),
("BUILD_BUILDID", "7"),
("SYSTEM_PULLREQUEST_PULLREQUESTID", "99"),
]);
let detected = detect_environment(env);
assert_eq!(detected.provider, EnvironmentProvider::AzureDevOps);
assert_eq!(detected.run_id.as_deref(), Some("7"));
assert_eq!(detected.pull_request.as_deref(), Some("99"));
}
#[test]
fn detect_environment_defaults_to_local() {
let detected = detect_environment(env_from(&[]));
assert_eq!(detected.provider, EnvironmentProvider::Local);
}
#[test]
fn provider_serializes_brand_names_without_mangling() {
let cases = [
(EnvironmentProvider::Local, "\"local\""),
(EnvironmentProvider::GitHubActions, "\"github_actions\""),
(EnvironmentProvider::AzureDevOps, "\"azure_devops\""),
];
for (provider, wire) in cases {
let serialized = serde_json::to_string(&provider).unwrap();
assert_eq!(serialized, wire);
let parsed: EnvironmentProvider = serde_json::from_str(wire).unwrap();
assert_eq!(parsed, provider);
}
}
}