use crate::core::Result;
use crate::engine::{Mission, MissionStep, StepType, MissionConfig};
use crate::transpiler::common::TranspilationContext;
use serde_json::{json, Value};
use std::collections::HashMap;
pub struct JenkinsParser;
#[derive(Debug, Clone)]
pub struct JenkinsStage {
pub name: String,
pub steps: Vec<JenkinsStep>,
pub when_condition: Option<String>,
pub parallel_stages: Vec<JenkinsStage>,
pub agent: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsStep {
pub step_type: String, pub parameters: HashMap<String, Value>,
pub script: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsPipeline {
pub pipeline_type: JenkinsPipelineType,
pub agent: Option<String>,
pub stages: Vec<JenkinsStage>,
pub post_actions: Vec<JenkinsStep>,
pub environment: HashMap<String, String>,
pub parameters: Vec<JenkinsParameter>,
}
#[derive(Debug, Clone)]
pub enum JenkinsPipelineType {
Declarative,
Scripted,
}
#[derive(Debug, Clone)]
pub struct JenkinsParameter {
pub name: String,
pub param_type: String, pub default_value: Option<String>,
pub description: Option<String>,
}
impl JenkinsParser {
pub async fn parse_file(file_path: &str) -> Result<Mission> {
let content = std::fs::read_to_string(file_path)?;
Self::parse_string(&content).await
}
pub async fn parse_string(content: &str) -> Result<Mission> {
let mut context = TranspilationContext::new("Jenkins Pipeline Mission".to_string());
let pipeline = Self::parse_pipeline(content)?;
let mut steps = Vec::new();
let mut step_counter = 1;
if !pipeline.environment.is_empty() {
let env_step = Self::create_environment_setup_step(&pipeline.environment, &format!("env_{}", step_counter))?;
steps.push(env_step);
step_counter += 1;
}
for param in &pipeline.parameters {
let param_step = Self::create_parameter_step(param, &format!("param_{}", step_counter))?;
steps.push(param_step);
step_counter += 1;
}
for stage in &pipeline.stages {
let stage_steps = Self::create_stage_steps(stage, &mut step_counter)?;
steps.extend(stage_steps);
}
for post_action in &pipeline.post_actions {
let post_step = Self::create_post_action_step(post_action, &format!("post_{}", step_counter))?;
steps.push(post_step);
step_counter += 1;
}
context.add_variable("total_stages".to_string(), pipeline.stages.len().to_string());
context.add_variable("pipeline_type".to_string(), format!("{:?}", pipeline.pipeline_type));
context.add_variable("agent".to_string(), pipeline.agent.unwrap_or_else(|| "any".to_string()));
Ok(Mission {
version: "1.0".to_string(),
name: "Jenkins Pipeline Mission".to_string(),
description: Some(format!("Converted from Jenkins {:?} pipeline with {} stages",
pipeline.pipeline_type, pipeline.stages.len())),
steps,
config: Some(MissionConfig {
max_parallel_steps: Some(4), timeout_seconds: Some(3600), fail_fast: Some(true), }),
})
}
fn parse_pipeline(content: &str) -> Result<JenkinsPipeline> {
let is_declarative = content.contains("pipeline {") || content.contains("pipeline{");
let pipeline_type = if is_declarative {
JenkinsPipelineType::Declarative
} else {
JenkinsPipelineType::Scripted
};
let mut stages = Vec::new();
let mut environment = HashMap::new();
let parameters = Vec::new();
let mut agent = None;
let post_actions = Vec::new();
let lines: Vec<&str> = content.lines().map(|l| l.trim()).collect();
let mut i = 0;
let mut current_stage: Option<JenkinsStage> = None;
while i < lines.len() {
let line = lines[i];
if line.starts_with("agent ") {
agent = Self::parse_agent_line(line);
} else if line.starts_with("stage(") || line.contains("stage '") {
if let Some(stage) = current_stage.take() {
stages.push(stage);
}
current_stage = Self::parse_stage_start(line);
} else if line.starts_with("sh ") || line.starts_with("bat ") || line.starts_with("powershell ") {
if let Some(ref mut stage) = current_stage {
if let Some(step) = Self::parse_command_step(line)? {
stage.steps.push(step);
}
}
} else if line.starts_with("git ") || line.starts_with("checkout ") {
if let Some(ref mut stage) = current_stage {
if let Some(step) = Self::parse_scm_step(line)? {
stage.steps.push(step);
}
}
} else if line.contains("environment {") {
environment.insert("BUILD_ENV".to_string(), "jenkins".to_string());
}
i += 1;
}
if let Some(stage) = current_stage {
stages.push(stage);
}
if stages.is_empty() {
stages.push(JenkinsStage {
name: "Default".to_string(),
steps: vec![JenkinsStep {
step_type: "sh".to_string(),
parameters: HashMap::new(),
script: Some("echo 'No stages found in Jenkins pipeline'".to_string()),
}],
when_condition: None,
parallel_stages: Vec::new(),
agent: None,
});
}
Ok(JenkinsPipeline {
pipeline_type,
agent,
stages,
post_actions,
environment,
parameters,
})
}
fn parse_agent_line(line: &str) -> Option<String> {
if line.contains("any") {
Some("any".to_string())
} else if line.contains("docker") {
Some("docker".to_string())
} else if line.contains("kubernetes") {
Some("kubernetes".to_string())
} else {
Some("any".to_string())
}
}
fn parse_stage_start(line: &str) -> Option<JenkinsStage> {
let stage_name = if line.contains("stage(") {
line.split("stage(").nth(1)?
.split(")").next()?
.trim_matches(|c| c == '\'' || c == '"')
.to_string()
} else if line.contains("stage '") {
line.split("stage '").nth(1)?
.split("'").next()?
.to_string()
} else {
"Unknown".to_string()
};
Some(JenkinsStage {
name: stage_name,
steps: Vec::new(),
when_condition: None,
parallel_stages: Vec::new(),
agent: None,
})
}
fn parse_command_step(line: &str) -> Result<Option<JenkinsStep>> {
let (step_type, script) = if line.starts_with("sh ") {
("sh", line.strip_prefix("sh ").unwrap_or(""))
} else if line.starts_with("bat ") {
("bat", line.strip_prefix("bat ").unwrap_or(""))
} else if line.starts_with("powershell ") {
("powershell", line.strip_prefix("powershell ").unwrap_or(""))
} else {
return Ok(None);
};
let cleaned_script = script.trim_matches(|c| c == '\'' || c == '"' || c == '{' || c == '}').trim();
Ok(Some(JenkinsStep {
step_type: step_type.to_string(),
parameters: HashMap::new(),
script: Some(cleaned_script.to_string()),
}))
}
fn parse_scm_step(line: &str) -> Result<Option<JenkinsStep>> {
if line.starts_with("git ") || line.starts_with("checkout ") {
let mut parameters = HashMap::new();
if line.contains("http") || line.contains("git@") {
let url_start = line.find("http").or_else(|| line.find("git@"));
if let Some(start) = url_start {
let url_part = &line[start..];
let url_end = url_part.find("'").or_else(|| url_part.find("\"")).unwrap_or(url_part.len());
let url = &url_part[..url_end];
parameters.insert("url".to_string(), Value::String(url.to_string()));
}
}
Ok(Some(JenkinsStep {
step_type: "git".to_string(),
parameters,
script: None,
}))
} else {
Ok(None)
}
}
fn create_environment_setup_step(environment: &HashMap<String, String>, step_id: &str) -> Result<MissionStep> {
Ok(MissionStep {
id: step_id.to_string(),
name: "Setup Environment Variables".to_string(),
step_type: StepType::Noop,
depends_on: None,
timeout_seconds: Some(30),
continue_on_error: Some(false),
parameters: json!({
"action": "setup_environment",
"variables": environment,
"jenkins_environment": true
}),
})
}
fn create_parameter_step(param: &JenkinsParameter, step_id: &str) -> Result<MissionStep> {
Ok(MissionStep {
id: step_id.to_string(),
name: format!("Setup Parameter: {}", param.name),
step_type: StepType::Noop,
depends_on: None,
timeout_seconds: Some(30),
continue_on_error: Some(false),
parameters: json!({
"parameter_name": param.name,
"parameter_type": param.param_type,
"default_value": param.default_value,
"description": param.description,
"jenkins_parameter": true
}),
})
}
fn create_stage_steps(stage: &JenkinsStage, step_counter: &mut usize) -> Result<Vec<MissionStep>> {
let mut steps = Vec::new();
let stage_start = MissionStep {
id: format!("stage_{}", step_counter),
name: format!("Jenkins Stage: {}", stage.name),
step_type: StepType::Noop,
depends_on: None,
timeout_seconds: Some(30),
continue_on_error: Some(false),
parameters: json!({
"stage_name": stage.name,
"when_condition": stage.when_condition,
"jenkins_stage": true
}),
};
steps.push(stage_start);
*step_counter += 1;
for jenkins_step in &stage.steps {
let rustchain_step = Self::convert_jenkins_step_to_rustchain(jenkins_step, &format!("step_{}", step_counter))?;
steps.push(rustchain_step);
*step_counter += 1;
}
for parallel_stage in &stage.parallel_stages {
let parallel_steps = Self::create_stage_steps(parallel_stage, step_counter)?;
steps.extend(parallel_steps);
}
Ok(steps)
}
fn convert_jenkins_step_to_rustchain(jenkins_step: &JenkinsStep, step_id: &str) -> Result<MissionStep> {
let (step_type, timeout, parameters) = match jenkins_step.step_type.as_str() {
"sh" | "bat" | "powershell" => {
(StepType::Command, 300, json!({
"command": jenkins_step.script.clone().unwrap_or_else(|| "echo 'No command'".to_string()),
"shell": jenkins_step.step_type,
"jenkins_command": true
}))
},
"git" => {
(StepType::Http, 120, json!({
"action": "git_clone",
"parameters": jenkins_step.parameters,
"jenkins_scm": true
}))
},
"checkout" => {
(StepType::Http, 120, json!({
"action": "checkout",
"parameters": jenkins_step.parameters,
"jenkins_scm": true
}))
},
"build" => {
(StepType::Command, 1800, json!({
"action": "build",
"parameters": jenkins_step.parameters,
"jenkins_build": true
}))
},
"test" => {
(StepType::Command, 900, json!({
"action": "test",
"parameters": jenkins_step.parameters,
"jenkins_test": true
}))
},
_ => {
(StepType::Noop, 60, json!({
"step_type": jenkins_step.step_type,
"parameters": jenkins_step.parameters,
"script": jenkins_step.script,
"jenkins_custom": true
}))
}
};
Ok(MissionStep {
id: step_id.to_string(),
name: format!("Jenkins {}: {}",
jenkins_step.step_type.to_uppercase(),
jenkins_step.script.clone().unwrap_or_else(|| "Custom Step".to_string())[..50.min(jenkins_step.script.clone().unwrap_or_else(|| "Custom Step".to_string()).len())].to_string()),
step_type,
depends_on: None,
timeout_seconds: Some(timeout),
continue_on_error: Some(false),
parameters,
})
}
fn create_post_action_step(post_action: &JenkinsStep, step_id: &str) -> Result<MissionStep> {
Ok(MissionStep {
id: step_id.to_string(),
name: format!("Post Action: {}", post_action.step_type),
step_type: StepType::Noop,
depends_on: None,
timeout_seconds: Some(120),
continue_on_error: Some(true), parameters: json!({
"action": "post_action",
"step_type": post_action.step_type,
"parameters": post_action.parameters,
"jenkins_post": true
}),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_declarative_pipeline() {
let jenkins_pipeline = r#"
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
sh 'echo "Build complete"'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
}
}
"#;
let result = JenkinsParser::parse_string(jenkins_pipeline).await;
assert!(result.is_ok());
let mission = result.unwrap();
assert_eq!(mission.name, "Jenkins Pipeline Mission");
assert!(mission.steps.len() >= 2);
let step_names: Vec<String> = mission.steps.iter().map(|s| s.name.clone()).collect();
assert!(step_names.iter().any(|name| name.contains("Build")));
assert!(step_names.iter().any(|name| name.contains("Test")));
}
#[tokio::test]
async fn test_parse_scripted_pipeline() {
let jenkins_pipeline = r#"
node {
stage 'Checkout'
checkout scm
stage 'Build'
sh 'mvn clean compile'
stage 'Test'
sh 'mvn test'
}
"#;
let result = JenkinsParser::parse_string(jenkins_pipeline).await;
assert!(result.is_ok());
let mission = result.unwrap();
assert!(mission.description.unwrap().contains("Scripted pipeline"));
let step_names: Vec<String> = mission.steps.iter().map(|s| s.name.clone()).collect();
assert!(step_names.iter().any(|name| name.contains("Checkout")));
assert!(step_names.iter().any(|name| name.contains("Build")));
assert!(step_names.iter().any(|name| name.contains("Test")));
}
#[tokio::test]
async fn test_parse_pipeline_with_git() {
let jenkins_pipeline = r#"
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git 'https://github.com/example/repo.git'
}
}
}
}
"#;
let result = JenkinsParser::parse_string(jenkins_pipeline).await;
assert!(result.is_ok());
let mission = result.unwrap();
let git_steps: Vec<&MissionStep> = mission.steps.iter()
.filter(|step| matches!(step.step_type, StepType::Http))
.collect();
assert!(!git_steps.is_empty());
let git_step = git_steps.first().unwrap();
assert!(git_step.parameters.get("jenkins_scm").is_some());
}
#[test]
fn test_parse_command_step() {
let sh_line = "sh 'make build && make test'";
let result = JenkinsParser::parse_command_step(sh_line).unwrap();
assert!(result.is_some());
let step = result.unwrap();
assert_eq!(step.step_type, "sh");
assert_eq!(step.script.unwrap(), "make build && make test");
let bat_line = "bat \"build.cmd\"";
let result = JenkinsParser::parse_command_step(bat_line).unwrap();
assert!(result.is_some());
let step = result.unwrap();
assert_eq!(step.step_type, "bat");
assert_eq!(step.script.unwrap(), "build.cmd");
}
#[test]
fn test_parse_scm_step() {
let git_line = "git 'https://github.com/example/repo.git'";
let result = JenkinsParser::parse_scm_step(git_line).unwrap();
assert!(result.is_some());
let step = result.unwrap();
assert_eq!(step.step_type, "git");
let url = step.parameters.get("url");
assert!(url.is_some());
assert!(url.unwrap().as_str().unwrap().contains("github.com"));
}
#[tokio::test]
async fn test_empty_pipeline() {
let result = JenkinsParser::parse_string("").await;
assert!(result.is_ok());
let mission = result.unwrap();
assert!(mission.steps.len() >= 1); assert!(mission.description.unwrap().contains("1 stages")); }
#[tokio::test]
async fn test_pipeline_with_environment() {
let jenkins_pipeline = r#"
pipeline {
agent any
environment {
NODE_ENV = 'production'
API_KEY = credentials('api-key')
}
stages {
stage('Deploy') {
steps {
sh 'npm run deploy'
}
}
}
}
"#;
let result = JenkinsParser::parse_string(jenkins_pipeline).await;
assert!(result.is_ok());
let mission = result.unwrap();
let env_steps: Vec<&MissionStep> = mission.steps.iter()
.filter(|step| step.name.contains("Environment"))
.collect();
assert!(!env_steps.is_empty());
}
#[test]
fn test_stage_name_parsing() {
let declarative_stage = "stage('Build and Test') {";
let stage = JenkinsParser::parse_stage_start(declarative_stage);
assert!(stage.is_some());
assert_eq!(stage.unwrap().name, "Build and Test");
let scripted_stage = "stage 'Deploy to Production'";
let stage = JenkinsParser::parse_stage_start(scripted_stage);
assert!(stage.is_some());
assert_eq!(stage.unwrap().name, "Deploy to Production");
}
}