Skip to main content

mk_lib/schema/
mod.rs

1mod command;
2mod include;
3mod plan;
4mod precondition;
5mod shell;
6mod task;
7mod task_context;
8mod task_dependency;
9mod task_root;
10mod use_cargo;
11mod use_npm;
12mod validation;
13
14use std::collections::HashSet;
15use std::fmt;
16use std::process::Stdio;
17use std::sync::{
18  Arc,
19  Mutex,
20};
21
22use once_cell::sync::Lazy;
23use regex::Regex;
24
25pub type ActiveTasks = Arc<Mutex<HashSet<String>>>;
26pub type CompletedTasks = Arc<Mutex<HashSet<String>>>;
27
28#[derive(Debug)]
29pub struct ExecutionInterrupted;
30
31impl fmt::Display for ExecutionInterrupted {
32  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33    write!(f, "Execution interrupted")
34  }
35}
36
37impl std::error::Error for ExecutionInterrupted {}
38
39pub use command::*;
40pub use include::*;
41pub use plan::*;
42pub use precondition::*;
43pub use shell::*;
44pub use task::*;
45pub use task_context::*;
46pub use task_dependency::*;
47pub use task_root::*;
48pub use use_cargo::*;
49pub use use_npm::*;
50pub use validation::*;
51
52use crate::secrets::load_secret_value;
53
54static TEMPLATE_COMMAND_RE: Lazy<Regex> =
55  Lazy::new(|| Regex::new(r"^\$\{\{.+\}\}$").expect("valid template regex"));
56static TEMPLATE_EXPR_RE: Lazy<Regex> =
57  Lazy::new(|| Regex::new(r"\$\{\{\s*(.+?)\s*\}\}").expect("valid template expression regex"));
58
59pub fn is_shell_command(value: &str) -> anyhow::Result<bool> {
60  let re = Regex::new(r"^\$\(.+\)$")?;
61  Ok(re.is_match(value))
62}
63
64pub fn is_template_command(value: &str) -> anyhow::Result<bool> {
65  Ok(TEMPLATE_COMMAND_RE.is_match(value))
66}
67
68pub fn resolve_template_command_value(value: &str, context: &TaskContext) -> anyhow::Result<String> {
69  let value = value.trim_start_matches("${{").trim_end_matches("}}").trim();
70  resolve_template_expression(value, context)
71}
72
73pub fn resolve_template_expression(value: &str, context: &TaskContext) -> anyhow::Result<String> {
74  if value.starts_with("env.") {
75    let value = value.trim_start_matches("env.");
76    let value = context
77      .env_vars
78      .get(value)
79      .ok_or_else(|| anyhow::anyhow!("Environment variable '{}' is not defined", value))?;
80    Ok(value.to_string())
81  } else if value.starts_with("secrets.") {
82    let path = value.trim_start_matches("secrets.");
83    load_secret_value(
84      path,
85      &context.task_root.config_base_dir(),
86      context.secret_vault_location.as_deref(),
87      context.secret_keys_location.as_deref(),
88      context.secret_key_name.as_deref(),
89      context.secret_gpg_key_id.as_deref(),
90    )
91  } else if value.starts_with("outputs.") {
92    let name = value.trim_start_matches("outputs.");
93    context.get_task_output(name)?.ok_or_else(|| {
94      anyhow::anyhow!(
95        "Task output '{}' is not available. Ensure the task that produces it runs before this one.",
96        name
97      )
98    })
99  } else {
100    Ok(value.to_string())
101  }
102}
103
104pub fn interpolate_template_string(value: &str, context: &TaskContext) -> anyhow::Result<String> {
105  let mut result = String::with_capacity(value.len());
106  let mut last_end = 0usize;
107  for captures in TEMPLATE_EXPR_RE.captures_iter(value) {
108    let Some(full_match) = captures.get(0) else {
109      continue;
110    };
111    let Some(expr) = captures.get(1) else {
112      continue;
113    };
114    result.push_str(&value[last_end..full_match.start()]);
115    result.push_str(&resolve_template_expression(expr.as_str().trim(), context)?);
116    last_end = full_match.end();
117  }
118  result.push_str(&value[last_end..]);
119  Ok(result)
120}
121
122pub fn extract_output_references(value: &str) -> Vec<String> {
123  TEMPLATE_EXPR_RE
124    .captures_iter(value)
125    .filter_map(|captures| captures.get(1))
126    .map(|expr| expr.as_str().trim())
127    .filter_map(|expr| expr.strip_prefix("outputs."))
128    .map(str::to_string)
129    .collect()
130}
131
132pub fn contains_output_reference(value: &str) -> bool {
133  !extract_output_references(value).is_empty()
134}
135
136pub fn get_output_handler(verbose: bool) -> Stdio {
137  if verbose {
138    Stdio::piped()
139  } else {
140    Stdio::null()
141  }
142}
143
144#[cfg(test)]
145mod test {
146  use std::sync::Arc;
147
148  use super::*;
149
150  #[test]
151  fn test_interpolate_template_string_resolves_outputs() -> anyhow::Result<()> {
152    let root = Arc::new(TaskRoot::default());
153    let context = TaskContext::empty_with_root(root);
154    context.insert_task_output("version", "v1.2.3")?;
155    assert_eq!(
156      interpolate_template_string("tag=${{ outputs.version }}", &context)?,
157      "tag=v1.2.3"
158    );
159    Ok(())
160  }
161
162  #[test]
163  fn test_extract_output_references_finds_all_output_templates() {
164    assert_eq!(
165      extract_output_references("${{ outputs.first }}-${{ outputs.second }}"),
166      vec!["first".to_string(), "second".to_string()]
167    );
168  }
169}