use anyhow::{Context as _, Result};
use std::collections::HashMap;
use tera::Value;
use crate::env_source::{EnvSource, ProcessEnvSource};
use crate::template_preprocess::preprocess;
use super::base_tera::{BASE_TERA, translate_go_time_format};
use super::vars::{ENV_REF_RE, NUMERIC_FIELDS, TemplateVars};
fn build_tera_context_for_template(
vars: &TemplateVars,
preprocessed: &str,
host_env: &dyn EnvSource,
) -> tera::Context {
let referenced_env_keys: Vec<String> = ENV_REF_RE
.captures_iter(preprocessed)
.map(|cap| cap[1].to_string())
.collect();
let mut env_with_defaults = HashMap::new();
for key in &referenced_env_keys {
if !vars.env.contains_key(key.as_str()) {
let value = host_env.var(key).unwrap_or_default();
env_with_defaults.insert(key.clone(), value);
}
}
for (k, v) in &vars.env {
env_with_defaults.insert(k.clone(), v.clone());
}
let mut augmented_vars = vars.clone();
augmented_vars.env = env_with_defaults;
build_tera_context(&augmented_vars)
}
fn build_tera_context(vars: &TemplateVars) -> tera::Context {
let mut ctx = tera::Context::new();
for (k, v) in &vars.vars {
if NUMERIC_FIELDS.contains(&k.as_str())
&& let Ok(n) = v.parse::<i64>()
{
ctx.insert(k.as_str(), &n);
continue;
}
match v.as_str() {
"true" => ctx.insert(k.as_str(), &true),
"false" => ctx.insert(k.as_str(), &false),
_ => ctx.insert(k.as_str(), v),
}
}
ctx.insert("Env", &vars.env);
ctx.insert("Var", &vars.custom_vars);
ctx.insert("Outputs", &vars.outputs);
let mut runtime = HashMap::new();
if let Some(goos) = vars.vars.get("RuntimeGoos") {
runtime.insert("Goos".to_string(), goos.clone());
}
if let Some(goarch) = vars.vars.get("RuntimeGoarch") {
runtime.insert("Goarch".to_string(), goarch.clone());
}
if !runtime.is_empty() {
ctx.insert("Runtime", &runtime);
}
for (k, v) in &vars.structured {
ctx.insert(k.as_str(), v);
}
ctx
}
pub fn render(template: &str, vars: &TemplateVars) -> Result<String> {
render_with_env(template, vars, &ProcessEnvSource)
}
pub fn render_with_env(
template: &str,
vars: &TemplateVars,
host_env: &dyn EnvSource,
) -> Result<String> {
let preprocessed = preprocess(template);
let ctx = build_tera_context_for_template(vars, &preprocessed, host_env);
let mut tera = BASE_TERA.clone();
let host_snapshot: HashMap<String, String> = host_env.vars().into_iter().collect();
let host_snapshot = std::sync::Arc::new(host_snapshot);
let env_map = std::sync::Arc::new(vars.all_env().clone());
let env_map_for_default = env_map.clone();
let host_for_default = host_snapshot.clone();
tera.register_function(
"envOrDefault",
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("envOrDefault requires `name` argument"))?;
let default = args.get("default").and_then(|v| v.as_str()).unwrap_or("");
let value = env_map_for_default
.get(name)
.cloned()
.or_else(|| host_for_default.get(name).cloned())
.unwrap_or_else(|| default.to_string());
Ok(Value::String(value))
},
);
let env_map_for_isset = env_map.clone();
let host_for_isset = host_snapshot.clone();
tera.register_function(
"isEnvSet",
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("isEnvSet requires `name` argument"))?;
let is_set = env_map_for_isset
.get(name)
.map(|v| !v.is_empty())
.unwrap_or_else(|| {
host_for_isset
.get(name)
.map(|v| !v.is_empty())
.unwrap_or(false)
});
Ok(Value::Bool(is_set))
},
);
let host_for_time = host_snapshot.clone();
tera.register_function(
"time",
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
let fmt = args
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("%Y-%m-%dT%H:%M:%SZ");
let chrono_fmt = translate_go_time_format(fmt);
let sde = host_for_time.get("SOURCE_DATE_EPOCH").cloned();
let now = sde
.and_then(|s| s.parse::<i64>().ok())
.and_then(|secs| chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0))
.unwrap_or_else(chrono::Utc::now);
Ok(Value::String(now.format(&chrono_fmt).to_string()))
},
);
let host_for_now_format = host_snapshot.clone();
tera.register_filter(
"now_format",
move |_value: &Value, args: &HashMap<String, Value>| {
let fmt = args
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("now_format requires a `format` argument"))?;
let chrono_fmt = translate_go_time_format(fmt);
let sde = host_for_now_format.get("SOURCE_DATE_EPOCH").cloned();
let now = sde
.and_then(|s| s.parse::<i64>().ok())
.and_then(|secs| chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0))
.unwrap_or_else(chrono::Utc::now);
Ok(Value::String(now.format(&chrono_fmt).to_string()))
},
);
tera.add_raw_template("__inline__", &preprocessed)
.with_context(|| format!("failed to parse template: {}", template))?;
tera.render("__inline__", &ctx)
.with_context(|| format!("failed to render template: {}", template))
}
pub fn extract_artifact_ext(filename: &str) -> &str {
const TAR_COMPOUND_SUFFIXES: &[&str] = &[
".tar.gz", ".tar.xz", ".tar.zst", ".tar.bz2", ".tar.lz4", ".tar.sz",
];
let lower = filename.to_ascii_lowercase();
for suffix in TAR_COMPOUND_SUFFIXES {
if lower.ends_with(suffix) {
return &filename[filename.len() - suffix.len()..];
}
}
match filename.rfind('.') {
Some(pos) if pos > 0 => &filename[pos..],
_ => "",
}
}