use anyhow::{Context as _, Result};
use regex::Regex;
use std::collections::HashMap;
use std::sync::LazyLock;
use tera::Value;
use crate::template_preprocess::preprocess;
use super::base_tera::BASE_TERA;
use super::vars::{ENV_REF_RE, NUMERIC_FIELDS, TemplateVars};
fn build_tera_context_for_template(vars: &TemplateVars, preprocessed: &str) -> 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 = std::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> {
let preprocessed = preprocess(template);
let ctx = build_tera_context_for_template(vars, &preprocessed);
let mut tera = BASE_TERA.clone();
let env_map = std::sync::Arc::new(vars.all_env().clone());
let env_map_for_default = env_map.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(|| std::env::var(name).ok())
.unwrap_or_else(|| default.to_string());
Ok(Value::String(value))
},
);
let env_map_for_isset = env_map.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(|| std::env::var(name).map(|v| !v.is_empty()).unwrap_or(false));
Ok(Value::Bool(is_set))
},
);
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..],
_ => "",
}
}
pub fn validate_single_env_only(template: &str) -> Result<()> {
static ENV_ONLY_RE: LazyLock<Regex> = LazyLock::new(|| {
crate::util::static_regex(r"^\s*\{\{\s*\.?Env\.[A-Za-z_][A-Za-z0-9_]*\s*\}\}\s*$")
});
if ENV_ONLY_RE.is_match(template) {
Ok(())
} else {
anyhow::bail!(
"expected a single env var reference like '{{{{ .Env.VAR }}}}', got: {}",
template
)
}
}