1use crate::expression::{
5 DependenciesContext, ExpressionContext, ExpressionEngine, JobContext, JobDependency,
6 JobStatusContext, PipelineContext, StageContext, StageDependency, StepContext,
7 StepStatusContext,
8};
9use crate::parser::models::{
10 ExecutionContext, Job, JobResult, JobStatus, Pipeline, Stage, StageResult, StageStatus,
11 StepResult, StepStatus, Value, Variable,
12};
13
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
18pub struct RuntimeContext {
19 pub base: ExecutionContext,
21
22 pub current_stage: Option<String>,
24
25 pub current_job: Option<String>,
27
28 pub stage_results: HashMap<String, StageResult>,
30
31 pub job_results: HashMap<String, JobResult>,
33
34 pub step_results: Vec<StepResult>,
36
37 pub variables: HashMap<String, Value>,
39
40 pub parameters: HashMap<String, Value>,
42
43 pub env: HashMap<String, Value>,
45
46 pub step_outputs: HashMap<String, HashMap<String, Value>>,
48}
49
50impl RuntimeContext {
51 pub fn new(base: ExecutionContext) -> Self {
53 let variables: HashMap<String, Value> = base
54 .variables
55 .iter()
56 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
57 .collect();
58
59 let parameters: HashMap<String, Value> = base
60 .parameters
61 .iter()
62 .map(|(k, v)| (k.clone(), yaml_to_value(v)))
63 .collect();
64
65 Self {
66 base,
67 current_stage: None,
68 current_job: None,
69 stage_results: HashMap::new(),
70 job_results: HashMap::new(),
71 step_results: Vec::new(),
72 variables,
73 parameters,
74 env: HashMap::new(),
75 step_outputs: HashMap::new(),
76 }
77 }
78
79 pub fn from_pipeline(pipeline: &Pipeline, working_dir: String) -> Self {
81 let base = ExecutionContext::new(
82 pipeline
83 .name
84 .clone()
85 .unwrap_or_else(|| "unnamed".to_string()),
86 working_dir,
87 );
88
89 let mut ctx = Self::new(base);
90
91 ctx.merge_variables(&pipeline.variables);
93
94 for param in &pipeline.parameters {
96 if let Some(default) = ¶m.default {
97 ctx.parameters
98 .entry(param.name.clone())
99 .or_insert_with(|| yaml_to_value(default));
100 }
101 }
102
103 ctx
104 }
105
106 pub fn enter_stage(&mut self, stage: &Stage) {
108 self.current_stage = stage.stage.clone();
109 self.current_job = None;
110 self.step_results.clear();
111 self.step_outputs.clear();
112
113 self.merge_variables(&stage.variables);
115 }
116
117 pub fn exit_stage(&mut self, result: StageResult) {
119 if let Some(stage_name) = self.current_stage.take() {
120 self.stage_results.insert(stage_name, result);
121 }
122 }
123
124 pub fn enter_job(&mut self, job: &Job) {
126 self.current_job = job.identifier().map(|s| s.to_string());
127 self.step_results.clear();
128 self.step_outputs.clear();
129
130 self.merge_variables(&job.variables);
132 }
133
134 pub fn exit_job(&mut self, result: JobResult) {
136 let key = match (&self.current_stage, &self.current_job) {
137 (Some(stage), Some(job)) => format!("{}.{}", stage, job),
138 (None, Some(job)) => job.clone(),
139 _ => return,
140 };
141 self.job_results.insert(key, result);
142 self.current_job = None;
143 }
144
145 pub fn record_step_result(&mut self, result: StepResult) {
147 if let Some(step_name) = &result.step_name {
149 if !result.outputs.is_empty() {
150 self.step_outputs.insert(
151 step_name.clone(),
152 result
153 .outputs
154 .iter()
155 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
156 .collect(),
157 );
158 }
159 }
160
161 self.step_results.push(result);
162 }
163
164 pub fn set_variable(&mut self, name: String, value: Value) {
166 self.variables.insert(name, value);
167 }
168
169 pub fn set_step_output(&mut self, step_name: String, output_name: String, value: Value) {
171 self.step_outputs
172 .entry(step_name)
173 .or_default()
174 .insert(output_name, value);
175 }
176
177 pub fn set_env(&mut self, name: String, value: Value) {
179 self.env.insert(name, value);
180 }
181
182 fn merge_variables(&mut self, variables: &[Variable]) {
184 for var in variables {
185 match var {
186 Variable::KeyValue { name, value, .. } => {
187 let trimmed = value.trim();
188 if trimmed.starts_with("$[") && trimmed.ends_with(']') {
189 let inner = &trimmed[2..trimmed.len() - 1];
191 let engine = self.expression_engine();
192 match engine.evaluate_runtime(inner) {
193 Ok(result) => {
194 self.variables.insert(name.clone(), result);
195 }
196 Err(_) => {
197 self.variables
199 .insert(name.clone(), Value::String(value.clone()));
200 }
201 }
202 } else if trimmed.starts_with("${{") && trimmed.ends_with("}}") {
203 let inner = &trimmed[3..trimmed.len() - 2].trim();
206 let engine = self.expression_engine();
207 match engine.evaluate_compile_time(inner) {
208 Ok(result) => {
209 self.variables.insert(name.clone(), result);
210 }
211 Err(_) => {
212 self.variables
213 .insert(name.clone(), Value::String(value.clone()));
214 }
215 }
216 } else if trimmed.contains("${{") {
217 let engine = self.expression_engine();
220 match engine.substitute_macros(trimmed) {
221 Ok(result) => {
222 self.variables.insert(name.clone(), Value::String(result));
223 }
224 Err(_) => {
225 self.variables
226 .insert(name.clone(), Value::String(value.clone()));
227 }
228 }
229 } else {
230 self.variables
231 .insert(name.clone(), Value::String(value.clone()));
232 }
233 }
234 Variable::Group { .. } => {
235 }
238 Variable::Template { .. } => {
239 }
242 }
243 }
244 }
245
246 pub fn merge_pipeline_variables(&mut self, variables: &[Variable]) {
248 self.merge_variables(variables);
249 }
250
251 pub fn to_expression_context(&self) -> ExpressionContext {
253 let mut ctx = ExpressionContext {
254 variables: self.variables.clone(),
255 parameters: self.parameters.clone(),
256 pipeline: PipelineContext {
257 name: Some(self.base.pipeline_name.clone()),
258 workspace: Some(self.base.working_dir.clone()),
259 },
260 ..Default::default()
261 };
262
263 if let Some(stage_name) = &self.current_stage {
265 ctx.stage = Some(StageContext {
266 name: stage_name.clone(),
267 display_name: None, });
269 }
270
271 if let Some(job_name) = &self.current_job {
273 ctx.job = Some(JobContext {
274 name: job_name.clone(),
275 display_name: None,
276 agent: Default::default(),
277 status: self.current_job_status(),
278 });
279 }
280
281 for (step_name, outputs) in &self.step_outputs {
283 let step_status = self
284 .step_results
285 .iter()
286 .find(|r| r.step_name.as_deref() == Some(step_name))
287 .map(|r| StepStatusContext {
288 succeeded: r.status == StepStatus::Succeeded
289 || r.status == StepStatus::SucceededWithIssues,
290 failed: r.status == StepStatus::Failed,
291 skipped: r.status == StepStatus::Skipped,
292 })
293 .unwrap_or_default();
294
295 ctx.steps.insert(
296 step_name.clone(),
297 StepContext {
298 outputs: outputs.clone(),
299 status: step_status,
300 },
301 );
302 }
303
304 ctx.dependencies = self.build_dependencies_context();
306
307 ctx.env = self.env.clone();
309
310 ctx
311 }
312
313 pub fn expression_engine(&self) -> ExpressionEngine {
315 ExpressionEngine::new(self.to_expression_context())
316 }
317
318 pub fn evaluate_condition(&self, condition: &str) -> Result<bool, String> {
320 let engine = self.expression_engine();
321 engine
322 .evaluate_runtime(condition)
323 .map(|v| v.is_truthy())
324 .map_err(|e| e.message)
325 }
326
327 pub fn substitute_variables(&self, text: &str) -> Result<String, String> {
329 let engine = self.expression_engine();
330 engine.substitute_macros(text).map_err(|e| e.message)
331 }
332
333 fn current_job_status(&self) -> JobStatusContext {
335 let has_failed = self
337 .step_results
338 .iter()
339 .any(|r| r.status == StepStatus::Failed);
340
341 JobStatusContext {
342 succeeded: !has_failed && !self.step_results.is_empty(),
343 failed: has_failed,
344 canceled: false, }
346 }
347
348 fn build_dependencies_context(&self) -> DependenciesContext {
350 let mut ctx = DependenciesContext::default();
351
352 for (stage_name, result) in &self.stage_results {
354 let mut outputs = HashMap::new();
355
356 for (job_key, job_result) in &self.job_results {
358 if job_key.starts_with(&format!("{}.", stage_name)) {
359 let job_name = job_key.strip_prefix(&format!("{}.", stage_name)).unwrap();
360 outputs.insert(
361 job_name.to_string(),
362 job_result
363 .outputs
364 .iter()
365 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
366 .collect(),
367 );
368 }
369 }
370
371 ctx.stages.insert(
372 stage_name.clone(),
373 StageDependency {
374 outputs,
375 result: status_to_string(&result.status),
376 },
377 );
378 }
379
380 if let Some(current_stage) = &self.current_stage {
382 for (job_key, job_result) in &self.job_results {
383 if let Some(job_name) = job_key.strip_prefix(&format!("{}.", current_stage)) {
385 ctx.jobs.insert(
386 job_name.to_string(),
387 JobDependency {
388 outputs: job_result
389 .outputs
390 .iter()
391 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
392 .collect(),
393 result: job_status_to_string(&job_result.status),
394 },
395 );
396 }
397 }
398 }
399
400 ctx
401 }
402
403 pub fn dependencies_succeeded(&self, deps: &[String], is_stage: bool) -> bool {
405 if is_stage {
406 deps.iter().all(|dep| {
407 self.stage_results
408 .get(dep)
409 .map(|r| {
410 r.status == StageStatus::Succeeded
411 || r.status == StageStatus::SucceededWithIssues
412 })
413 .unwrap_or(false)
414 })
415 } else {
416 let stage_prefix = self
418 .current_stage
419 .as_ref()
420 .map(|s| format!("{}.", s))
421 .unwrap_or_default();
422
423 deps.iter().all(|dep| {
424 let key = format!("{}{}", stage_prefix, dep);
425 self.job_results
426 .get(&key)
427 .map(|r| {
428 r.status == JobStatus::Succeeded
429 || r.status == JobStatus::SucceededWithIssues
430 })
431 .unwrap_or(false)
432 })
433 }
434 }
435
436 pub fn env_as_strings(&self) -> HashMap<String, String> {
438 let mut env = HashMap::new();
439
440 for (k, v) in &self.base.env {
442 env.insert(k.clone(), v.clone());
443 }
444
445 for (k, v) in &self.env {
447 env.insert(k.clone(), v.as_string());
448 }
449
450 env.insert(
452 "BUILD_SOURCESDIRECTORY".to_string(),
453 self.base.working_dir.clone(),
454 );
455 env.insert(
456 "SYSTEM_DEFAULTWORKINGDIRECTORY".to_string(),
457 self.base.working_dir.clone(),
458 );
459 env.insert(
460 "PIPELINE_WORKSPACE".to_string(),
461 self.base.working_dir.clone(),
462 );
463
464 if let Some(stage) = &self.current_stage {
465 env.insert("SYSTEM_STAGENAME".to_string(), stage.clone());
466 env.insert("SYSTEM_STAGEDISPLAYNAME".to_string(), stage.clone());
467 }
468
469 if let Some(job) = &self.current_job {
470 env.insert("SYSTEM_JOBNAME".to_string(), job.clone());
471 env.insert("SYSTEM_JOBDISPLAYNAME".to_string(), job.clone());
472 }
473
474 env
475 }
476}
477
478fn yaml_to_value(yaml: &serde_yaml::Value) -> Value {
480 match yaml {
481 serde_yaml::Value::Null => Value::Null,
482 serde_yaml::Value::Bool(b) => Value::Bool(*b),
483 serde_yaml::Value::Number(n) => {
484 Value::Number(n.as_f64().unwrap_or(n.as_i64().unwrap_or(0) as f64))
485 }
486 serde_yaml::Value::String(s) => Value::String(s.clone()),
487 serde_yaml::Value::Sequence(seq) => Value::Array(seq.iter().map(yaml_to_value).collect()),
488 serde_yaml::Value::Mapping(map) => Value::Object(
489 map.iter()
490 .filter_map(|(k, v)| k.as_str().map(|key| (key.to_string(), yaml_to_value(v))))
491 .collect(),
492 ),
493 serde_yaml::Value::Tagged(_) => Value::Null,
494 }
495}
496
497fn status_to_string(status: &StageStatus) -> String {
498 match status {
499 StageStatus::Succeeded => "Succeeded".to_string(),
500 StageStatus::SucceededWithIssues => "SucceededWithIssues".to_string(),
501 StageStatus::Failed => "Failed".to_string(),
502 StageStatus::Canceled => "Canceled".to_string(),
503 StageStatus::Skipped => "Skipped".to_string(),
504 StageStatus::Pending | StageStatus::Running => "InProgress".to_string(),
505 }
506}
507
508fn job_status_to_string(status: &JobStatus) -> String {
509 match status {
510 JobStatus::Succeeded => "Succeeded".to_string(),
511 JobStatus::SucceededWithIssues => "SucceededWithIssues".to_string(),
512 JobStatus::Failed => "Failed".to_string(),
513 JobStatus::Canceled => "Canceled".to_string(),
514 JobStatus::Skipped => "Skipped".to_string(),
515 JobStatus::Pending | JobStatus::Running => "InProgress".to_string(),
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use std::time::Duration;
523
524 #[test]
525 fn test_runtime_context_creation() {
526 let base = ExecutionContext::new("test-pipeline".to_string(), "/work".to_string());
527 let ctx = RuntimeContext::new(base);
528
529 assert_eq!(ctx.base.pipeline_name, "test-pipeline");
530 assert_eq!(ctx.base.working_dir, "/work");
531 assert!(ctx.current_stage.is_none());
532 assert!(ctx.current_job.is_none());
533 }
534
535 #[test]
536 fn test_enter_exit_stage() {
537 let base = ExecutionContext::new("test".to_string(), "/work".to_string());
538 let mut ctx = RuntimeContext::new(base);
539
540 let stage = Stage {
541 stage: Some("Build".to_string()),
542 display_name: None,
543 depends_on: Default::default(),
544 condition: None,
545 variables: vec![Variable::KeyValue {
546 name: "stage_var".to_string(),
547 value: "stage_value".to_string(),
548 readonly: false,
549 }],
550 jobs: Vec::new(),
551 lock_behavior: None,
552 template: None,
553 parameters: HashMap::new(),
554 pool: None,
555 has_template_directives: false,
556 };
557
558 ctx.enter_stage(&stage);
559 assert_eq!(ctx.current_stage, Some("Build".to_string()));
560 assert_eq!(
561 ctx.variables.get("stage_var"),
562 Some(&Value::String("stage_value".to_string()))
563 );
564
565 let result = StageResult {
566 stage_name: "Build".to_string(),
567 display_name: None,
568 status: StageStatus::Succeeded,
569 jobs: Vec::new(),
570 duration: Duration::from_secs(10),
571 };
572
573 ctx.exit_stage(result);
574 assert!(ctx.current_stage.is_none());
575 assert!(ctx.stage_results.contains_key("Build"));
576 }
577
578 #[test]
579 fn test_evaluate_condition() {
580 let mut base = ExecutionContext::new("test".to_string(), "/work".to_string());
581 base.variables
582 .insert("isRelease".to_string(), "true".to_string());
583
584 let ctx = RuntimeContext::new(base);
585
586 assert!(ctx
587 .evaluate_condition("eq(variables.isRelease, 'true')")
588 .unwrap());
589 assert!(!ctx
590 .evaluate_condition("eq(variables.isRelease, 'false')")
591 .unwrap());
592 }
593
594 #[test]
595 fn test_substitute_variables() {
596 let mut base = ExecutionContext::new("test".to_string(), "/work".to_string());
597 base.variables
598 .insert("version".to_string(), "1.0.0".to_string());
599
600 let ctx = RuntimeContext::new(base);
601
602 let result = ctx.substitute_variables("Version: $(version)").unwrap();
603 assert_eq!(result, "Version: 1.0.0");
604 }
605
606 #[test]
607 fn test_step_outputs() {
608 let base = ExecutionContext::new("test".to_string(), "/work".to_string());
609 let mut ctx = RuntimeContext::new(base);
610
611 ctx.set_step_output(
612 "GetVersion".to_string(),
613 "version".to_string(),
614 Value::String("2.0.0".to_string()),
615 );
616
617 let expr_ctx = ctx.to_expression_context();
618 let step_ctx = expr_ctx.steps.get("GetVersion").unwrap();
619 assert_eq!(
620 step_ctx.outputs.get("version"),
621 Some(&Value::String("2.0.0".to_string()))
622 );
623 }
624
625 #[test]
626 fn test_dependencies_succeeded() {
627 let base = ExecutionContext::new("test".to_string(), "/work".to_string());
628 let mut ctx = RuntimeContext::new(base);
629
630 ctx.stage_results.insert(
632 "Build".to_string(),
633 StageResult {
634 stage_name: "Build".to_string(),
635 display_name: None,
636 status: StageStatus::Succeeded,
637 jobs: Vec::new(),
638 duration: Duration::from_secs(10),
639 },
640 );
641
642 assert!(ctx.dependencies_succeeded(&["Build".to_string()], true));
643 assert!(!ctx.dependencies_succeeded(&["Test".to_string()], true));
644 }
645}