1
mod command;
2
mod include;
3
mod plan;
4
mod precondition;
5
mod shell;
6
mod task;
7
mod task_context;
8
mod task_dependency;
9
mod task_root;
10
mod use_cargo;
11
mod use_npm;
12
mod validation;
13

            
14
use std::collections::HashSet;
15
use std::fmt;
16
use std::process::Stdio;
17
use std::sync::{
18
  Arc,
19
  Mutex,
20
};
21

            
22
use once_cell::sync::Lazy;
23
use regex::Regex;
24

            
25
pub type ActiveTasks = Arc<Mutex<HashSet<String>>>;
26
pub type CompletedTasks = Arc<Mutex<HashSet<String>>>;
27

            
28
#[derive(Debug)]
29
pub struct ExecutionInterrupted;
30

            
31
impl fmt::Display for ExecutionInterrupted {
32
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33
    write!(f, "Execution interrupted")
34
  }
35
}
36

            
37
impl std::error::Error for ExecutionInterrupted {}
38

            
39
pub use command::*;
40
pub use include::*;
41
pub use plan::*;
42
pub use precondition::*;
43
pub use shell::*;
44
pub use task::*;
45
pub use task_context::*;
46
pub use task_dependency::*;
47
pub use task_root::*;
48
pub use use_cargo::*;
49
pub use use_npm::*;
50
pub use validation::*;
51

            
52
use crate::secrets::load_secret_value;
53

            
54
static TEMPLATE_COMMAND_RE: Lazy<Regex> =
55
  Lazy::new(|| Regex::new(r"^\$\{\{.+\}\}$").expect("valid template regex"));
56
static TEMPLATE_EXPR_RE: Lazy<Regex> =
57
114
  Lazy::new(|| Regex::new(r"\$\{\{\s*(.+?)\s*\}\}").expect("valid template expression regex"));
58

            
59
pub fn is_shell_command(value: &str) -> anyhow::Result<bool> {
60
  let re = Regex::new(r"^\$\(.+\)$")?;
61
  Ok(re.is_match(value))
62
}
63

            
64
pub fn is_template_command(value: &str) -> anyhow::Result<bool> {
65
  Ok(TEMPLATE_COMMAND_RE.is_match(value))
66
}
67

            
68
pub 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

            
73
2
pub fn resolve_template_expression(value: &str, context: &TaskContext) -> anyhow::Result<String> {
74
2
  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
2
  } else if value.starts_with("secrets.") {
82
    let path = value.trim_start_matches("secrets.");
83
    load_secret_value(
84
      path,
85
      context
86
        .secret_config
87
        .as_ref()
88
        .ok_or_else(|| anyhow::anyhow!("Secret config missing from task context"))?,
89
    )
90
2
  } else if value.starts_with("outputs.") {
91
2
    let name = value.trim_start_matches("outputs.");
92
2
    context.get_task_output(name)?.ok_or_else(|| {
93
      anyhow::anyhow!(
94
        "Task output '{}' is not available. Ensure the task that produces it runs before this one.",
95
        name
96
      )
97
    })
98
  } else {
99
    Ok(value.to_string())
100
  }
101
2
}
102

            
103
74
pub fn interpolate_template_string(value: &str, context: &TaskContext) -> anyhow::Result<String> {
104
74
  let mut result = String::with_capacity(value.len());
105
74
  let mut last_end = 0usize;
106
74
  for captures in TEMPLATE_EXPR_RE.captures_iter(value) {
107
2
    let Some(full_match) = captures.get(0) else {
108
      continue;
109
    };
110
2
    let Some(expr) = captures.get(1) else {
111
      continue;
112
    };
113
2
    result.push_str(&value[last_end..full_match.start()]);
114
2
    result.push_str(&resolve_template_expression(expr.as_str().trim(), context)?);
115
2
    last_end = full_match.end();
116
  }
117
74
  result.push_str(&value[last_end..]);
118
74
  Ok(result)
119
74
}
120

            
121
128
pub fn extract_output_references(value: &str) -> Vec<String> {
122
128
  TEMPLATE_EXPR_RE
123
128
    .captures_iter(value)
124
128
    .filter_map(|captures| captures.get(1))
125
128
    .map(|expr| expr.as_str().trim())
126
128
    .filter_map(|expr| expr.strip_prefix("outputs."))
127
128
    .map(str::to_string)
128
128
    .collect()
129
128
}
130

            
131
pub fn contains_output_reference(value: &str) -> bool {
132
  !extract_output_references(value).is_empty()
133
}
134

            
135
196
pub fn get_output_handler(verbose: bool) -> Stdio {
136
196
  if verbose {
137
112
    Stdio::piped()
138
  } else {
139
84
    Stdio::null()
140
  }
141
196
}
142

            
143
#[cfg(test)]
144
mod test {
145
  use std::sync::Arc;
146

            
147
  use super::*;
148

            
149
  #[test]
150
2
  fn test_interpolate_template_string_resolves_outputs() -> anyhow::Result<()> {
151
2
    let root = Arc::new(TaskRoot::default());
152
2
    let context = TaskContext::empty_with_root(root);
153
2
    context.insert_task_output("version", "v1.2.3")?;
154
2
    assert_eq!(
155
2
      interpolate_template_string("tag=${{ outputs.version }}", &context)?,
156
      "tag=v1.2.3"
157
    );
158
2
    Ok(())
159
2
  }
160

            
161
  #[test]
162
2
  fn test_extract_output_references_finds_all_output_templates() {
163
2
    assert_eq!(
164
2
      extract_output_references("${{ outputs.first }}-${{ outputs.second }}"),
165
2
      vec!["first".to_string(), "second".to_string()]
166
    );
167
2
  }
168
}