Skip to main content

cuenv_ci/executor/
config.rs

1//! CI Executor Configuration
2//!
3//! Configuration for the CI pipeline executor including cache settings,
4//! parallelism, and secret handling.
5
6use crate::ir::CachePolicy;
7use cuenv_core::{DryRun, OutputCapture};
8use std::path::PathBuf;
9
10/// Default shell path for task execution
11pub const DEFAULT_SHELL: &str = "/bin/sh";
12
13/// Configuration for the CI executor
14#[derive(Debug, Clone)]
15pub struct CIExecutorConfig {
16    /// Project root directory
17    pub project_root: PathBuf,
18
19    /// Cache root directory (default: .cuenv/cache)
20    pub cache_root: Option<PathBuf>,
21
22    /// Maximum parallel tasks (0 = unlimited)
23    pub max_parallel: usize,
24
25    /// Capture stdout/stderr for reports
26    pub capture_output: OutputCapture,
27
28    /// Dry run mode (don't execute, just report what would run)
29    pub dry_run: DryRun,
30
31    /// Global cache policy override (for fork PRs -> Readonly)
32    pub cache_policy_override: Option<CachePolicy>,
33
34    /// Salt for secret fingerprinting (`CUENV_SECRET_SALT`)
35    pub secret_salt: Option<String>,
36
37    /// Previous salt for secret fingerprinting during rotation (`CUENV_SECRET_SALT_PREV`)
38    pub secret_salt_prev: Option<String>,
39
40    /// Shell path for shell-mode task execution (default: /bin/sh)
41    pub shell_path: String,
42}
43
44impl Default for CIExecutorConfig {
45    fn default() -> Self {
46        Self {
47            project_root: PathBuf::from("."),
48            cache_root: None,
49            max_parallel: 4,
50            capture_output: OutputCapture::Capture,
51            dry_run: DryRun::No,
52            cache_policy_override: None,
53            secret_salt: None,
54            secret_salt_prev: None,
55            shell_path: DEFAULT_SHELL.to_string(),
56        }
57    }
58}
59
60impl CIExecutorConfig {
61    /// Create a new config with the given project root
62    #[must_use]
63    pub fn new(project_root: PathBuf) -> Self {
64        Self {
65            project_root,
66            ..Default::default()
67        }
68    }
69
70    /// Set the cache root directory
71    #[must_use]
72    pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self {
73        self.cache_root = Some(cache_root);
74        self
75    }
76
77    /// Set the maximum parallel tasks
78    #[must_use]
79    pub const fn with_max_parallel(mut self, max_parallel: usize) -> Self {
80        self.max_parallel = max_parallel;
81        self
82    }
83
84    /// Enable or disable output capture
85    #[must_use]
86    pub const fn with_capture_output(mut self, capture: OutputCapture) -> Self {
87        self.capture_output = capture;
88        self
89    }
90
91    /// Enable or disable dry run mode
92    #[must_use]
93    pub const fn with_dry_run(mut self, dry_run: DryRun) -> Self {
94        self.dry_run = dry_run;
95        self
96    }
97
98    /// Set a global cache policy override
99    #[must_use]
100    pub const fn with_cache_policy_override(mut self, policy: CachePolicy) -> Self {
101        self.cache_policy_override = Some(policy);
102        self
103    }
104
105    /// Set the secret salt for fingerprinting
106    #[must_use]
107    pub fn with_secret_salt(mut self, salt: String) -> Self {
108        self.secret_salt = Some(salt);
109        self
110    }
111
112    /// Set the previous secret salt for rotation
113    #[must_use]
114    pub fn with_secret_salt_prev(mut self, salt: String) -> Self {
115        self.secret_salt_prev = Some(salt);
116        self
117    }
118
119    /// Set the shell path for shell-mode execution
120    #[must_use]
121    pub fn with_shell_path(mut self, shell_path: impl Into<String>) -> Self {
122        self.shell_path = shell_path.into();
123        self
124    }
125
126    /// Get the effective cache root path
127    #[must_use]
128    pub fn effective_cache_root(&self) -> PathBuf {
129        self.cache_root
130            .clone()
131            .unwrap_or_else(|| self.project_root.join(".cuenv/cache"))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_default_config() {
141        let config = CIExecutorConfig::default();
142        assert_eq!(config.max_parallel, 4);
143        assert!(config.capture_output.should_capture());
144        assert!(!config.dry_run.is_dry_run());
145        assert!(config.cache_policy_override.is_none());
146    }
147
148    #[test]
149    fn test_builder_pattern() {
150        let config = CIExecutorConfig::new(PathBuf::from("/project"))
151            .with_max_parallel(8)
152            .with_dry_run(DryRun::Yes)
153            .with_cache_policy_override(CachePolicy::Readonly);
154
155        assert_eq!(config.max_parallel, 8);
156        assert!(config.dry_run.is_dry_run());
157        assert_eq!(config.cache_policy_override, Some(CachePolicy::Readonly));
158    }
159
160    #[test]
161    fn test_effective_cache_root() {
162        let config = CIExecutorConfig::new(PathBuf::from("/project"));
163        assert_eq!(
164            config.effective_cache_root(),
165            PathBuf::from("/project/.cuenv/cache")
166        );
167
168        let config_with_override = CIExecutorConfig::new(PathBuf::from("/project"))
169            .with_cache_root(PathBuf::from("/tmp/cache"));
170        assert_eq!(
171            config_with_override.effective_cache_root(),
172            PathBuf::from("/tmp/cache")
173        );
174    }
175
176    #[test]
177    fn test_shell_path_default() {
178        let config = CIExecutorConfig::default();
179        assert_eq!(config.shell_path, "/bin/sh");
180    }
181
182    #[test]
183    fn test_shell_path_custom() {
184        let config = CIExecutorConfig::new(PathBuf::from("/project")).with_shell_path("/bin/bash");
185        assert_eq!(config.shell_path, "/bin/bash");
186    }
187}