use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiSystem {
GitHubActions,
GitLabCI,
Jenkins,
CircleCI,
TravisCI,
AzurePipelines,
DroneCI,
Buildkite,
TeamCity,
BitbucketPipelines,
Custom,
}
impl CiSystem {
pub fn name(&self) -> &'static str {
match self {
Self::GitHubActions => "GitHub Actions",
Self::GitLabCI => "GitLab CI",
Self::Jenkins => "Jenkins",
Self::CircleCI => "CircleCI",
Self::TravisCI => "Travis CI",
Self::AzurePipelines => "Azure Pipelines",
Self::DroneCI => "Drone CI",
Self::Buildkite => "Buildkite",
Self::TeamCity => "TeamCity",
Self::BitbucketPipelines => "Bitbucket Pipelines",
Self::Custom => "Custom CI",
}
}
pub fn config_file(&self) -> &'static str {
match self {
Self::GitHubActions => ".github/workflows/*.yml",
Self::GitLabCI => ".gitlab-ci.yml",
Self::Jenkins => "Jenkinsfile",
Self::CircleCI => ".circleci/config.yml",
Self::TravisCI => ".travis.yml",
Self::AzurePipelines => "azure-pipelines.yml",
Self::DroneCI => ".drone.yml",
Self::Buildkite => "pipeline.yml",
Self::TeamCity => ".teamcity/settings.kts",
Self::BitbucketPipelines => "bitbucket-pipelines.yml",
Self::Custom => "ci.yml",
}
}
}
#[derive(Debug, Clone)]
pub struct CiJobResult {
pub job_name: String,
pub stage: String,
pub status: CiJobStatus,
pub exit_code: Option<i32>,
pub duration_secs: f64,
pub started_at: Option<SystemTime>,
pub finished_at: Option<SystemTime>,
pub artifacts: Vec<CiArtifact>,
pub logs_url: Option<String>,
pub test_summary: Option<CiTestSummary>,
pub compiler_output: Option<String>,
pub warnings: Vec<CiWarning>,
pub errors: Vec<CiError>,
pub retry_count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiJobStatus {
Pending,
Running,
Success,
Failed,
Skipped,
Cancelled,
TimedOut,
Manual,
Created,
}
impl CiJobStatus {
pub fn as_emoji(&self) -> &'static str {
match self {
Self::Pending => "⏳",
Self::Running => "🔄",
Self::Success => "✅",
Self::Failed => "❌",
Self::Skipped => "⏭️",
Self::Cancelled => "🚫",
Self::TimedOut => "⏰",
Self::Manual => "👆",
Self::Created => "🆕",
}
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Success | Self::Failed | Self::Skipped | Self::Cancelled | Self::TimedOut
)
}
}
#[derive(Debug, Clone)]
pub struct CiTestSummary {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub errors: usize,
pub duration_secs: f64,
pub test_suites: Vec<CiTestSuite>,
pub coverage: Option<f64>,
}
impl Default for CiTestSummary {
fn default() -> Self {
Self {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
errors: 0,
duration_secs: 0.0,
test_suites: Vec::new(),
coverage: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CiTestSuite {
pub name: String,
pub total: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub duration_secs: f64,
pub test_cases: Vec<CiTestCase>,
}
#[derive(Debug, Clone)]
pub struct CiTestCase {
pub name: String,
pub classname: String,
pub passed: bool,
pub skipped: bool,
pub duration_secs: f64,
pub error_message: Option<String>,
pub stack_trace: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct CiArtifact {
pub name: String,
pub path: PathBuf,
pub size_bytes: u64,
pub artifact_type: CiArtifactType,
pub compressed: bool,
pub expires_in: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiArtifactType {
Binary,
Library,
TestReport,
CoverageReport,
Logs,
PerformanceData,
Documentation,
ContainerImage,
Package,
Other,
}
#[derive(Debug, Clone)]
pub struct CiWarning {
pub message: String,
pub file: Option<String>,
pub line: Option<u32>,
pub category: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CiError {
pub message: String,
pub file: Option<String>,
pub line: Option<u32>,
pub error_code: Option<String>,
pub stack_trace: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GitHubWorkflow {
pub name: String,
pub on: GitHubTrigger,
pub env: HashMap<String, String>,
pub jobs: HashMap<String, GitHubJob>,
pub permissions: HashMap<String, String>,
pub concurrency: Option<GitHubConcurrency>,
}
#[derive(Debug, Clone)]
pub struct GitHubTrigger {
pub push: Option<GitHubPushTrigger>,
pub pull_request: Option<GitHubPullRequestTrigger>,
pub schedule: Option<Vec<GitHubScheduleTrigger>>,
pub workflow_dispatch: Option<bool>,
pub workflow_call: Option<bool>,
pub release: Option<GitHubReleaseTrigger>,
}
#[derive(Debug, Clone)]
pub struct GitHubPushTrigger {
pub branches: Vec<String>,
pub tags: Vec<String>,
pub paths: Vec<String>,
pub paths_ignore: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GitHubPullRequestTrigger {
pub branches: Vec<String>,
pub types: Vec<String>,
pub paths: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GitHubScheduleTrigger {
pub cron: String,
}
#[derive(Debug, Clone)]
pub struct GitHubReleaseTrigger {
pub types: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GitHubConcurrency {
pub group: String,
pub cancel_in_progress: bool,
}
#[derive(Debug, Clone)]
pub struct GitHubJob {
pub name: String,
pub runs_on: String,
pub needs: Vec<String>,
pub if_condition: Option<String>,
pub env: HashMap<String, String>,
pub steps: Vec<GitHubStep>,
pub strategy: Option<GitHubStrategy>,
pub timeout_minutes: Option<u32>,
pub container: Option<GitHubContainer>,
pub services: HashMap<String, GitHubService>,
pub concurrency: Option<GitHubConcurrency>,
pub outputs: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct GitHubStep {
pub id: Option<String>,
pub name: Option<String>,
pub uses: Option<String>,
pub run: Option<String>,
pub shell: Option<String>,
pub with: HashMap<String, String>,
pub env: HashMap<String, String>,
pub if_condition: Option<String>,
pub continue_on_error: Option<bool>,
pub timeout_minutes: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct GitHubStrategy {
pub matrix: HashMap<String, Vec<String>>,
pub fail_fast: bool,
pub max_parallel: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct GitHubContainer {
pub image: String,
pub env: HashMap<String, String>,
pub options: String,
pub credentials: Option<GitHubContainerCredentials>,
}
#[derive(Debug, Clone)]
pub struct GitHubContainerCredentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub struct GitHubService {
pub image: String,
pub env: HashMap<String, String>,
pub ports: Vec<String>,
pub options: String,
}
pub fn parse_github_workflow(path: &Path) -> Result<GitHubWorkflow, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let workflow = GitHubWorkflow {
name: "CI".to_string(),
on: GitHubTrigger {
push: Some(GitHubPushTrigger {
branches: vec!["main".to_string(), "develop".to_string()],
tags: vec!["v*".to_string()],
paths: vec!["src/**".to_string()],
paths_ignore: vec!["docs/**".to_string()],
}),
pull_request: Some(GitHubPullRequestTrigger {
branches: vec!["main".to_string()],
types: vec!["opened".to_string(), "synchronize".to_string()],
paths: vec!["src/**".to_string()],
}),
schedule: None,
workflow_dispatch: Some(true),
workflow_call: None,
release: None,
},
env: HashMap::new(),
jobs: HashMap::new(),
permissions: HashMap::new(),
concurrency: None,
};
Ok(workflow)
}
pub fn generate_github_workflow(
name: &str,
matrix_os: &[&str],
matrix_compiler: &[&str],
matrix_build_type: &[&str],
cmake_args: &[&str],
test_command: Option<&str>,
) -> String {
let mut yaml = String::new();
yaml.push_str(&format!("name: {}\n\n", name));
yaml.push_str("on:\n");
yaml.push_str(" push:\n");
yaml.push_str(" branches: [main, develop]\n");
yaml.push_str(" pull_request:\n");
yaml.push_str(" branches: [main]\n\n");
yaml.push_str("jobs:\n");
yaml.push_str(" build:\n");
yaml.push_str(&format!(" name: Build\n"));
yaml.push_str(" runs-on: ${{ matrix.os }}\n\n");
yaml.push_str(" strategy:\n");
yaml.push_str(" fail-fast: false\n");
yaml.push_str(" matrix:\n");
yaml.push_str(&format!(" os: {}\n", serde_json::to_string(matrix_os).unwrap_or_default()));
yaml.push_str(&format!(
" compiler: {}\n",
serde_json::to_string(matrix_compiler).unwrap_or_default()
));
yaml.push_str(&format!(
" build_type: {}\n",
serde_json::to_string(matrix_build_type).unwrap_or_default()
));
yaml.push_str("\n");
yaml.push_str(" steps:\n");
yaml.push_str(" - uses: actions/checkout@v4\n\n");
yaml.push_str(" - name: Install dependencies\n");
yaml.push_str(" run: |\n");
yaml.push_str(" sudo apt-get update\n");
yaml.push_str(" sudo apt-get install -y cmake ninja-build\n\n");
yaml.push_str(" - name: Configure CMake\n");
yaml.push_str(" run: |\n");
yaml.push_str(" cmake -B build -G Ninja \\\n");
yaml.push_str(" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \\\n");
yaml.push_str(" -DCMAKE_C_COMPILER=${{ matrix.compiler }} \\\n");
yaml.push_str(" -DCMAKE_CXX_COMPILER=${{ matrix.compiler }}++ \\\n");
for arg in cmake_args {
yaml.push_str(&format!(" {} \\\n", arg));
}
yaml.push_str("\n");
yaml.push_str(" - name: Build\n");
yaml.push_str(" run: cmake --build build -j$(nproc)\n\n");
if let Some(test_cmd) = test_command {
yaml.push_str(" - name: Test\n");
yaml.push_str(&format!(" run: {}\n\n", test_cmd));
}
yaml.push_str(" - name: Upload artifacts\n");
yaml.push_str(" uses: actions/upload-artifact@v4\n");
yaml.push_str(" with:\n");
yaml.push_str(" name: build-${{ matrix.os }}\n");
yaml.push_str(" path: build/\n");
yaml
}
pub fn github_actions_build(workflow: &GitHubWorkflow) -> Vec<CiJobResult> {
let mut results = Vec::new();
for (job_id, job) in &workflow.jobs {
let mut result = CiJobResult {
job_name: job.name.clone(),
stage: "build".to_string(),
status: CiJobStatus::Running,
exit_code: None,
duration_secs: 0.0,
started_at: Some(SystemTime::now()),
finished_at: None,
artifacts: Vec::new(),
logs_url: None,
test_summary: None,
compiler_output: None,
warnings: Vec::new(),
errors: Vec::new(),
retry_count: 0,
};
for step in &job.steps {
if let Some(ref run_cmd) = step.run {
let output = ProcessCommand::new("sh")
.arg("-c")
.arg(run_cmd)
.output();
match output {
Ok(out) if out.status.success() => {
result.compiler_output = Some(String::from_utf8_lossy(&out.stdout).to_string());
}
Ok(out) => {
result.status = CiJobStatus::Failed;
result.exit_code = out.status.code();
result.errors.push(CiError {
message: String::from_utf8_lossy(&out.stderr).to_string(),
file: None,
line: None,
error_code: None,
stack_trace: None,
});
}
Err(e) => {
result.status = CiJobStatus::Failed;
result.errors.push(CiError {
message: format!("Step execution error: {}", e),
file: None,
line: None,
error_code: None,
stack_trace: None,
});
}
}
}
}
if result.status == CiJobStatus::Running {
result.status = CiJobStatus::Success;
}
result.finished_at = Some(SystemTime::now());
result.duration_secs = 0.0;
results.push(result);
}
results
}
#[derive(Debug, Clone)]
pub struct GitLabPipeline {
pub stages: Vec<String>,
pub variables: HashMap<String, String>,
pub default: Option<GitLabDefault>,
pub include: Vec<GitLabInclude>,
pub jobs: HashMap<String, GitLabJob>,
pub cache: Option<GitLabCache>,
pub workflows: Vec<GitLabWorkflow>,
}
#[derive(Debug, Clone)]
pub struct GitLabDefault {
pub image: String,
pub tags: Vec<String>,
pub before_script: Vec<String>,
pub after_script: Vec<String>,
pub retry: Option<GitLabRetry>,
pub interruptible: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct GitLabInclude {
pub local: Option<String>,
pub file: Option<String>,
pub project: Option<String>,
pub ref_branch: Option<String>,
pub remote: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GitLabJob {
pub stage: String,
pub image: Option<String>,
pub script: Vec<String>,
pub before_script: Vec<String>,
pub after_script: Vec<String>,
pub variables: HashMap<String, String>,
pub tags: Vec<String>,
pub only: Vec<String>,
pub except: Vec<String>,
pub needs: Vec<String>,
pub artifacts: Option<GitLabArtifacts>,
pub cache: Option<GitLabCache>,
pub rules: Vec<GitLabRule>,
pub retry: Option<GitLabRetry>,
pub timeout: Option<String>,
pub parallel: Option<u32>,
pub environment: Option<GitLabEnvironment>,
pub services: Vec<GitLabService>,
pub coverage: Option<String>,
pub dependencies: Vec<String>,
pub allow_failure: Option<bool>,
pub when: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GitLabArtifacts {
pub paths: Vec<String>,
pub expire_in: String,
pub reports: Option<GitLabReports>,
pub when: String,
pub name: Option<String>,
pub untracked: bool,
pub exclude: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GitLabReports {
pub junit: Option<String>,
pub coverage_report: Option<GitLabCoverageReport>,
pub dotenv: Option<String>,
pub sast: Option<String>,
pub secret_detection: Option<String>,
pub dependency_scanning: Option<String>,
pub container_scanning: Option<String>,
pub license_scanning: Option<String>,
pub browser_performance: Option<String>,
pub load_performance: Option<String>,
pub metrics: Option<String>,
pub requirements: Option<String>,
pub accessibility: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GitLabCoverageReport {
pub coverage_format: String,
pub path: String,
}
#[derive(Debug, Clone)]
pub struct GitLabCache {
pub key: String,
pub paths: Vec<String>,
pub policy: String, pub untracked: bool,
pub when: String,
}
#[derive(Debug, Clone)]
pub struct GitLabRule {
pub if_condition: Option<String>,
pub changes: Option<Vec<String>>,
pub exists: Option<Vec<String>>,
pub when: Option<String>,
pub allow_failure: Option<bool>,
pub variables: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct GitLabRetry {
pub max: u32,
pub when: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GitLabEnvironment {
pub name: String,
pub url: Option<String>,
pub on_stop: Option<String>,
pub auto_stop_in: Option<String>,
pub action: Option<String>,
pub kubernetes: Option<GitLabKubernetes>,
}
#[derive(Debug, Clone)]
pub struct GitLabKubernetes {
pub namespace: String,
}
#[derive(Debug, Clone)]
pub struct GitLabService {
pub name: String,
pub alias: Option<String>,
pub entrypoint: Option<Vec<String>>,
pub command: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct GitLabWorkflow {
pub rules: Vec<GitLabRule>,
}
pub fn parse_gitlab_ci(path: &Path) -> Result<GitLabPipeline, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let pipeline = GitLabPipeline {
stages: vec![
"build".to_string(),
"test".to_string(),
"deploy".to_string(),
],
variables: HashMap::new(),
default: Some(GitLabDefault {
image: "gcc:13".to_string(),
tags: vec!["docker".to_string()],
before_script: vec![
"apt-get update".to_string(),
"apt-get install -y cmake ninja-build".to_string(),
],
after_script: Vec::new(),
retry: Some(GitLabRetry {
max: 2,
when: vec!["runner_system_failure".to_string()],
}),
interruptible: Some(true),
}),
include: Vec::new(),
jobs: HashMap::new(),
cache: None,
workflows: Vec::new(),
};
Ok(pipeline)
}
pub fn generate_gitlab_ci(
image: &str,
build_stages: &[&str],
cmake_args: &[&str],
parallel_test_jobs: u32,
) -> String {
let mut yaml = String::new();
yaml.push_str("image: ");
yaml.push_str(image);
yaml.push_str("\n\n");
yaml.push_str("stages:\n");
for stage in build_stages {
yaml.push_str(&format!(" - {}\n", stage));
}
yaml.push_str("\n");
yaml.push_str("before_script:\n");
yaml.push_str(" - apt-get update -qq\n");
yaml.push_str(" - apt-get install -y -qq cmake ninja-build\n\n");
yaml.push_str("build:\n");
yaml.push_str(" stage: build\n");
yaml.push_str(" script:\n");
yaml.push_str(" - cmake -B build -G Ninja \\\n");
for arg in cmake_args {
yaml.push_str(&format!(" {} \\\n", arg));
}
yaml.push_str(" - cmake --build build\n");
yaml.push_str(" artifacts:\n");
yaml.push_str(" paths:\n");
yaml.push_str(" - build/\n\n");
yaml.push_str("test:\n");
yaml.push_str(" stage: test\n");
yaml.push_str(" dependencies:\n");
yaml.push_str(" - build\n");
yaml.push_str(" script:\n");
yaml.push_str(" - cd build && ctest --output-on-failure\n");
yaml.push_str(&format!(" parallel: {}\n", parallel_test_jobs));
yaml.push_str(" artifacts:\n");
yaml.push_str(" when: always\n");
yaml.push_str(" reports:\n");
yaml.push_str(" junit: build/test-results/*.xml\n");
yaml
}
#[derive(Debug, Clone)]
pub struct JenkinsPipeline {
pub agent: JenkinsAgent,
pub environment: HashMap<String, String>,
pub stages: Vec<JenkinsStage>,
pub post: Option<JenkinsPost>,
pub options: Vec<JenkinsOption>,
pub triggers: Vec<JenkinsTrigger>,
pub parameters: Vec<JenkinsParameter>,
pub tools: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsAgent {
pub label: Option<String>,
pub docker: Option<JenkinsDockerAgent>,
pub kubernetes: Option<JenkinsKubernetesAgent>,
pub node_label: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsDockerAgent {
pub image: String,
pub args: String,
pub always_pull: bool,
pub registry_url: Option<String>,
pub registry_credentials_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsKubernetesAgent {
pub cloud: String,
pub yaml: String,
}
#[derive(Debug, Clone)]
pub struct JenkinsStage {
pub name: String,
pub agent: Option<JenkinsAgent>,
pub when: Option<JenkinsWhen>,
pub steps: Vec<JenkinsStep>,
pub post: Option<JenkinsPost>,
pub parallel: Option<Vec<JenkinsStage>>,
pub environment: HashMap<String, String>,
pub options: Vec<JenkinsOption>,
pub tools: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsStep {
pub step_type: JenkinsStepType,
pub name: Option<String>,
}
#[derive(Debug, Clone)]
pub enum JenkinsStepType {
Sh(String),
Bat(String),
Dir { path: String, steps: Vec<JenkinsStep> },
Echo(String),
Error(String),
Script(String),
Checkout(Vec<String>),
Stash { name: String, includes: String },
Unstash(String),
ArchiveArtifacts { artifacts: String, fingerprint: bool },
JUnit(String),
PublishHtml { report_dir: String, report_files: String, report_name: String },
Cobertura(String),
RecordIssues { tool: String, pattern: String },
Parallel(Vec<JenkinsStage>),
Sleep { time: u32, unit: String },
Retry { count: u32, steps: Vec<JenkinsStep> },
Timeout { time: u32, unit: String, steps: Vec<JenkinsStep> },
}
#[derive(Debug, Clone)]
pub struct JenkinsPost {
pub always: Option<Vec<JenkinsStep>>,
pub success: Option<Vec<JenkinsStep>>,
pub failure: Option<Vec<JenkinsStep>>,
pub aborted: Option<Vec<JenkinsStep>>,
pub unstable: Option<Vec<JenkinsStep>>,
pub changed: Option<Vec<JenkinsStep>>,
pub fixed: Option<Vec<JenkinsStep>>,
}
#[derive(Debug, Clone)]
pub struct JenkinsWhen {
pub branch: Option<String>,
pub environment: Option<HashMap<String, String>>,
pub expression: Option<String>,
pub not: Option<Box<JenkinsWhen>>,
pub all_of: Option<Vec<JenkinsWhen>>,
pub any_of: Option<Vec<JenkinsWhen>>,
pub equals: Option<JenkinsWhenEquals>,
pub building_tag: Option<bool>,
pub tag: Option<String>,
pub change_request: Option<JenkinsChangeRequest>,
pub triggered_by: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JenkinsWhenEquals {
pub expected: String,
pub actual: String,
}
#[derive(Debug, Clone)]
pub struct JenkinsChangeRequest {
pub id: Option<String>,
pub target: Option<String>,
pub branch: Option<String>,
pub fork: Option<String>,
pub url: Option<String>,
pub title: Option<String>,
}
#[derive(Debug, Clone)]
pub enum JenkinsOption {
BuildDiscarder { days_to_keep: u32, num_to_keep: u32 },
DisableConcurrentBuilds,
OverrideIndexTriggers(bool),
SkipDefaultCheckout,
SkipStagesAfterUnstable,
Timeout { time: u32, unit: String },
Retry(u32),
Timestamps,
AnsiColor(String),
TimeoutActivity { time: u32 },
Lock(String),
NewContainerPerStage,
PipelineLibraries(Vec<String>),
QuietPeriod(u32),
PreserveStashes(u32),
}
#[derive(Debug, Clone)]
pub enum JenkinsTrigger {
Cron(String),
PollSCM(String),
Upstream { jobs: Vec<String>, threshold: String },
GenericWebhook { token: String },
GitHubPush,
GitHubPullRequest,
GitLabMR,
}
#[derive(Debug, Clone)]
pub enum JenkinsParameter {
StringParam { name: String, default: String, description: String },
BooleanParam { name: String, default: bool, description: String },
Choice { name: String, choices: Vec<String>, description: String },
File { name: String, description: String },
Password { name: String, default: String, description: String },
}
pub fn parse_jenkinsfile(path: &Path) -> Result<JenkinsPipeline, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let pipeline = JenkinsPipeline {
agent: JenkinsAgent {
label: Some("linux".to_string()),
docker: None,
kubernetes: None,
node_label: None,
},
environment: HashMap::new(),
stages: vec![
JenkinsStage {
name: "Build".to_string(),
agent: None,
when: None,
steps: vec![
JenkinsStep {
step_type: JenkinsStepType::Sh(
"cmake -B build && cmake --build build".to_string(),
),
name: Some("CMake Build".to_string()),
},
],
post: None,
parallel: None,
environment: HashMap::new(),
options: Vec::new(),
tools: HashMap::new(),
},
],
post: Some(JenkinsPost {
always: Some(vec![JenkinsStep {
step_type: JenkinsStepType::JUnit("build/test-results/*.xml".to_string()),
name: Some("Publish Test Results".to_string()),
}]),
success: None,
failure: None,
aborted: None,
unstable: None,
changed: None,
fixed: None,
}),
options: vec![
JenkinsOption::Timeout { time: 60, unit: "MINUTES".to_string() },
JenkinsOption::Timestamps,
],
triggers: vec![],
parameters: vec![],
tools: HashMap::new(),
};
Ok(pipeline)
}
pub fn generate_jenkinsfile(
docker_image: &str,
cmake_generator: &str,
build_type: &str,
test_parallelism: u32,
) -> String {
let mut groovy = String::new();
groovy.push_str("pipeline {\n");
groovy.push_str(" agent {\n");
groovy.push_str(&format!(" docker {{ image '{}' }}\n", docker_image));
groovy.push_str(" }\n\n");
groovy.push_str(" options {\n");
groovy.push_str(" timeout(time: 60, unit: 'MINUTES')\n");
groovy.push_str(" timestamps()\n");
groovy.push_str(" disableConcurrentBuilds()\n");
groovy.push_str(" }\n\n");
groovy.push_str(" stages {\n");
groovy.push_str(" stage('Configure') {\n");
groovy.push_str(" steps {\n");
groovy.push_str(" sh '''\n");
groovy.push_str(&format!(" cmake -B build -G \"{}\" \\\n", cmake_generator));
groovy.push_str(&format!(" -DCMAKE_BUILD_TYPE={} \\\n", build_type));
groovy.push_str(" '''\n");
groovy.push_str(" }\n");
groovy.push_str(" }\n\n");
groovy.push_str(" stage('Build') {\n");
groovy.push_str(" steps {\n");
groovy.push_str(" sh 'cmake --build build'\n");
groovy.push_str(" }\n");
groovy.push_str(" }\n\n");
groovy.push_str(" stage('Test') {\n");
groovy.push_str(" steps {\n");
groovy.push_str(&format!(
" sh 'cd build && ctest -j{} --output-on-failure'\n",
test_parallelism
));
groovy.push_str(" }\n");
groovy.push_str(" }\n");
groovy.push_str(" }\n\n");
groovy.push_str(" post {\n");
groovy.push_str(" always {\n");
groovy.push_str(" junit 'build/test-results/*.xml'\n");
groovy.push_str(" }\n");
groovy.push_str(" }\n");
groovy.push_str("}\n");
groovy
}
#[derive(Debug, Clone)]
pub struct CircleCIConfig {
pub version: String,
pub jobs: HashMap<String, CircleCIJob>,
pub workflows: HashMap<String, CircleCIWorkflow>,
pub orbs: HashMap<String, String>,
pub commands: HashMap<String, CircleCICommand>,
pub executors: HashMap<String, CircleCIExecutor>,
pub parameters: HashMap<String, CircleCIParameter>,
}
#[derive(Debug, Clone)]
pub struct CircleCIJob {
pub executor: CircleCIExecutor,
pub steps: Vec<CircleCIStep>,
pub parallelism: Option<u32>,
pub environment: HashMap<String, String>,
pub resource_class: Option<String>,
pub shell: Option<String>,
pub working_directory: Option<String>,
pub branches: Option<CircleCIBranches>,
}
#[derive(Debug, Clone)]
pub struct CircleCIExecutor {
pub executor_type: CircleCIExecutorType,
pub resource_class: String,
}
#[derive(Debug, Clone)]
pub enum CircleCIExecutorType {
Docker { image: String, auth: Option<CircleCIDockerAuth> },
Machine { image: String, docker_layer_caching: bool },
MacOS { xcode: String },
Windows,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct CircleCIDockerAuth {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub enum CircleCIStep {
Run {
name: Option<String>,
command: String,
shell: Option<String>,
environment: HashMap<String, String>,
when: Option<String>,
background: Option<bool>,
no_output_timeout: Option<String>,
},
Checkout,
SetupRemoteDocker {
docker_layer_caching: bool,
},
AddSshKeys {
fingerprints: Vec<String>,
},
SaveCache {
key: String,
paths: Vec<String>,
},
RestoreCache {
keys: Vec<String>,
},
StoreArtifacts {
path: String,
destination: Option<String>,
},
StoreTestResults {
path: String,
},
PersistToWorkspace {
root: String,
paths: Vec<String>,
},
AttachWorkspace {
at: String,
},
}
#[derive(Debug, Clone)]
pub struct CircleCIWorkflow {
pub jobs: Vec<CircleCIWorkflowJob>,
pub triggers: Option<Vec<CircleCITrigger>>,
pub when: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CircleCIWorkflowJob {
pub job_name: String,
pub name: Option<String>,
pub requires: Vec<String>,
pub filters: Option<CircleCIFilters>,
pub matrix: Option<CircleCIMatrix>,
pub pre_steps: Option<Vec<CircleCIStep>>,
pub post_steps: Option<Vec<CircleCIStep>>,
pub context: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CircleCIFilters {
pub branches: Option<CircleCIBranches>,
pub tags: Option<CircleCITags>,
}
#[derive(Debug, Clone)]
pub struct CircleCIBranches {
pub only: Vec<String>,
pub ignore: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CircleCITags {
pub only: Vec<String>,
pub ignore: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CircleCIMatrix {
pub parameters: HashMap<String, Vec<String>>,
pub alias: Option<String>,
pub exclude: Vec<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct CircleCITrigger {
pub schedule: Option<CircleCISchedule>,
}
#[derive(Debug, Clone)]
pub struct CircleCISchedule {
pub cron: String,
pub filters: Option<CircleCIFilters>,
}
#[derive(Debug, Clone)]
pub struct CircleCICommand {
pub parameters: HashMap<String, String>,
pub steps: Vec<CircleCIStep>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CircleCIParameter {
pub parameter_type: String,
pub default: Option<String>,
pub description: Option<String>,
pub enum_values: Option<Vec<String>>,
}
pub fn parse_circleci_config(path: &Path) -> Result<CircleCIConfig, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let config = CircleCIConfig {
version: "2.1".to_string(),
jobs: HashMap::new(),
workflows: HashMap::new(),
orbs: HashMap::new(),
commands: HashMap::new(),
executors: HashMap::new(),
parameters: HashMap::new(),
};
Ok(config)
}
#[derive(Debug, Clone)]
pub struct TravisConfig {
pub language: String,
pub os: Vec<String>,
pub dist: Option<String>,
pub compiler: Vec<String>,
pub env: HashMap<String, String>,
pub before_install: Vec<String>,
pub install: Vec<String>,
pub before_script: Vec<String>,
pub script: Vec<String>,
pub after_success: Vec<String>,
pub after_failure: Vec<String>,
pub before_deploy: Vec<String>,
pub deploy: Option<TravisDeploy>,
pub matrix: Option<Vec<HashMap<String, String>>>,
pub branches: Option<TravisBranches>,
pub cache: Option<TravisCache>,
pub addons: Option<TravisAddons>,
}
#[derive(Debug, Clone)]
pub struct TravisDeploy {
pub provider: String,
pub on: HashMap<String, String>,
pub settings: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct TravisBranches {
pub only: Vec<String>,
pub except: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct TravisCache {
pub directories: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct TravisAddons {
pub apt: Option<TravisAptAddon>,
pub homebrew: Option<TravisHomebrewAddon>,
pub artifacts: Option<TravisArtifactsAddon>,
pub sonarcloud: Option<TravisSonarAddon>,
}
#[derive(Debug, Clone)]
pub struct TravisAptAddon {
pub packages: Vec<String>,
pub sources: Vec<TravisAptSource>,
}
#[derive(Debug, Clone)]
pub struct TravisAptSource {
pub sourceline: String,
pub key_url: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TravisHomebrewAddon {
pub packages: Vec<String>,
pub update: bool,
}
#[derive(Debug, Clone)]
pub struct TravisArtifactsAddon {
pub bucket: String,
pub key: String,
pub secret: String,
pub paths: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct TravisSonarAddon {
pub organization: String,
pub token: String,
}
pub fn parse_travis_yml(path: &Path) -> Result<TravisConfig, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let config = TravisConfig {
language: "cpp".to_string(),
os: vec!["linux".to_string()],
dist: Some("jammy".to_string()),
compiler: vec!["clang".to_string(), "gcc".to_string()],
env: HashMap::new(),
before_install: vec!["sudo apt-get update -qq".to_string()],
install: vec![
"sudo apt-get install -y cmake ninja-build".to_string(),
],
before_script: Vec::new(),
script: vec![
"cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release".to_string(),
"cmake --build build".to_string(),
"cd build && ctest --output-on-failure".to_string(),
],
after_success: Vec::new(),
after_failure: Vec::new(),
before_deploy: Vec::new(),
deploy: None,
matrix: None,
branches: Some(TravisBranches {
only: vec!["main".to_string()],
except: Vec::new(),
}),
cache: Some(TravisCache {
directories: vec!["$HOME/.ccache".to_string()],
}),
addons: Some(TravisAddons {
apt: Some(TravisAptAddon {
packages: vec!["cmake".to_string(), "ninja-build".to_string()],
sources: vec![TravisAptSource {
sourceline: "ppa:ubuntu-toolchain-r/test".to_string(),
key_url: None,
}],
}),
homebrew: None,
artifacts: None,
sonarcloud: None,
}),
};
Ok(config)
}
#[derive(Debug, Clone)]
pub struct AzurePipelinesConfig {
pub trigger: Option<AzureTrigger>,
pub pr: Option<AzurePRTrigger>,
pub schedules: Vec<AzureSchedule>,
pub pool: AzurePool,
pub variables: HashMap<String, String>,
pub stages: Vec<AzureStage>,
pub jobs: Vec<AzureJob>,
pub steps: Vec<AzureStep>,
}
#[derive(Debug, Clone)]
pub struct AzureTrigger {
pub branches: AzureBranches,
pub paths: AzurePaths,
pub tags: AzureTags,
}
#[derive(Debug, Clone)]
pub struct AzureBranches {
pub include: Vec<String>,
pub exclude: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AzurePaths {
pub include: Vec<String>,
pub exclude: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AzureTags {
pub include: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AzurePRTrigger {
pub branches: AzureBranches,
pub paths: Option<AzurePaths>,
pub drafts: bool,
}
#[derive(Debug, Clone)]
pub struct AzureSchedule {
pub cron: String,
pub display_name: String,
pub branches: AzureBranches,
pub always: bool,
}
#[derive(Debug, Clone)]
pub struct AzurePool {
pub vm_image: String,
pub demands: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AzureStage {
pub stage: String,
pub display_name: Option<String>,
pub depends_on: Vec<String>,
pub condition: Option<String>,
pub variables: HashMap<String, String>,
pub jobs: Vec<AzureJob>,
}
#[derive(Debug, Clone)]
pub struct AzureJob {
pub job: String,
pub display_name: Option<String>,
pub depends_on: Vec<String>,
pub condition: Option<String>,
pub strategy: Option<AzureStrategy>,
pub pool: Option<AzurePool>,
pub timeout_in_minutes: Option<u32>,
pub variables: HashMap<String, String>,
pub steps: Vec<AzureStep>,
}
#[derive(Debug, Clone)]
pub struct AzureStrategy {
pub matrix: HashMap<String, HashMap<String, String>>,
pub parallel: u32,
}
#[derive(Debug, Clone)]
pub enum AzureStep {
Script {
display_name: Option<String>,
script: String,
working_directory: Option<String>,
condition: Option<String>,
continue_on_error: Option<bool>,
env: HashMap<String, String>,
},
Bash {
display_name: Option<String>,
bash: String,
working_directory: Option<String>,
},
Powershell {
display_name: Option<String>,
powershell: String,
error_action_preference: Option<String>,
},
PublishBuildArtifacts {
display_name: Option<String>,
path_to_publish: String,
artifact_name: String,
artifact_type: String,
},
PublishTestResults {
display_name: Option<String>,
test_results_format: String,
test_results_files: String,
merge_test_results: bool,
test_run_title: Option<String>,
},
PublishCodeCoverageResults {
display_name: Option<String>,
code_coverage_tool: String,
summary_file_location: String,
},
Task {
task: String,
inputs: HashMap<String, String>,
display_name: Option<String>,
},
Checkout {
path: Option<String>,
clean: bool,
fetch_depth: Option<u32>,
lfs: bool,
},
}
pub fn parse_azure_pipelines(path: &Path) -> Result<AzurePipelinesConfig, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let config = AzurePipelinesConfig {
trigger: Some(AzureTrigger {
branches: AzureBranches {
include: vec!["main".to_string()],
exclude: Vec::new(),
},
paths: AzurePaths {
include: vec!["src/**".to_string()],
exclude: Vec::new(),
},
tags: AzureTags {
include: vec!["v*".to_string()],
},
}),
pr: Some(AzurePRTrigger {
branches: AzureBranches {
include: vec!["main".to_string()],
exclude: Vec::new(),
},
paths: None,
drafts: true,
}),
schedules: vec![],
pool: AzurePool {
vm_image: "ubuntu-latest".to_string(),
demands: Vec::new(),
},
variables: HashMap::new(),
stages: vec![],
jobs: vec![],
steps: vec![
AzureStep::Script {
display_name: Some("Build with CMake".to_string()),
script: "cmake -B build && cmake --build build".to_string(),
working_directory: None,
condition: None,
continue_on_error: None,
env: HashMap::new(),
},
],
};
Ok(config)
}
#[derive(Debug, Clone)]
pub struct DronePipeline {
pub kind: String,
pub name: Option<String>,
pub type_: String, pub platform: Option<DronePlatform>,
pub steps: Vec<DroneStep>,
pub trigger: Option<DroneTrigger>,
pub depends_on: Vec<String>,
pub node: Option<HashMap<String, String>>,
pub volumes: Vec<DroneVolume>,
pub clone: Option<DroneClone>,
}
#[derive(Debug, Clone)]
pub struct DronePlatform {
pub os: String,
pub arch: String,
}
#[derive(Debug, Clone)]
pub struct DroneStep {
pub name: String,
pub image: String,
pub commands: Vec<String>,
pub environment: HashMap<String, String>,
pub volumes: Vec<DroneVolumeMount>,
pub depends_on: Vec<String>,
pub when: Option<DroneWhen>,
pub failure: Option<String>,
pub detach: Option<bool>,
pub privileged: Option<bool>,
pub user: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DroneVolumeMount {
pub name: String,
pub path: String,
}
#[derive(Debug, Clone)]
pub struct DroneTrigger {
pub branch: Option<Vec<String>>,
pub event: Option<Vec<String>>,
pub refs: Option<Vec<String>>,
pub repo: Option<Vec<String>>,
pub cron: Option<Vec<String>>,
pub target: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct DroneWhen {
pub branch: Option<Vec<String>>,
pub event: Option<Vec<String>>,
pub status: Option<Vec<String>>,
pub environment: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct DroneVolume {
pub name: String,
pub host: Option<DroneHostVolume>,
pub temp: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct DroneHostVolume {
pub path: String,
}
#[derive(Debug, Clone)]
pub struct DroneClone {
pub depth: Option<u32>,
pub disable: Option<bool>,
}
pub fn parse_drone_yml(path: &Path) -> Result<DronePipeline, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let pipeline = DronePipeline {
kind: "pipeline".to_string(),
name: Some("default".to_string()),
type_: "docker".to_string(),
platform: Some(DronePlatform {
os: "linux".to_string(),
arch: "amd64".to_string(),
}),
steps: vec![DroneStep {
name: "build".to_string(),
image: "gcc:13".to_string(),
commands: vec![
"cmake -B build".to_string(),
"cmake --build build".to_string(),
],
environment: HashMap::new(),
volumes: vec![],
depends_on: vec![],
when: None,
failure: None,
detach: None,
privileged: None,
user: None,
}],
trigger: Some(DroneTrigger {
branch: Some(vec!["main".to_string()]),
event: Some(vec!["push".to_string(), "pull_request".to_string()]),
refs: None,
repo: None,
cron: None,
target: None,
}),
depends_on: vec![],
node: None,
volumes: vec![],
clone: None,
};
Ok(pipeline)
}
#[derive(Debug, Clone)]
pub struct BuildkitePipeline {
pub steps: Vec<BuildkiteStep>,
pub env: HashMap<String, String>,
pub agents: HashMap<String, String>,
pub notify: Vec<BuildkiteNotify>,
}
#[derive(Debug, Clone)]
pub struct BuildkiteStep {
pub label: Option<String>,
pub command: String,
pub key: Option<String>,
pub depends_on: Option<Vec<String>>,
pub allow_dependency_failure: Option<bool>,
pub branches: Option<String>,
pub if_condition: Option<String>,
pub agents: Option<HashMap<String, String>>,
pub artifact_paths: Option<String>,
pub parallelism: Option<u32>,
pub timeout_in_minutes: Option<u32>,
pub retry: Option<BuildkiteRetry>,
pub plugins: Vec<BuildkitePlugin>,
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct BuildkiteRetry {
pub automatic: Option<BuildkiteAutomaticRetry>,
pub manual: Option<BuildkiteManualRetry>,
}
#[derive(Debug, Clone)]
pub struct BuildkiteAutomaticRetry {
pub limit: u32,
pub exit_status: Option<Vec<i32>>,
pub signal: Option<Vec<String>>,
pub signal_reason: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct BuildkiteManualRetry {
pub allowed: bool,
pub reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct BuildkitePlugin {
pub name: String,
pub config: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct BuildkiteNotify {
pub slack: Option<String>,
pub email: Option<String>,
pub basecamp_campfire: Option<String>,
pub webhook: Option<String>,
pub if_condition: Option<String>,
}
pub fn parse_buildkite_pipeline(path: &Path) -> Result<BuildkitePipeline, String> {
let _content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let pipeline = BuildkitePipeline {
steps: vec![BuildkiteStep {
label: Some("Build".to_string()),
command: "cmake -B build && cmake --build build".to_string(),
key: None,
depends_on: None,
allow_dependency_failure: None,
branches: None,
if_condition: None,
agents: Some(HashMap::from([("queue".to_string(), "default".to_string())])),
artifact_paths: Some("build/**/*".to_string()),
parallelism: None,
timeout_in_minutes: Some(30),
retry: Some(BuildkiteRetry {
automatic: Some(BuildkiteAutomaticRetry {
limit: 2,
exit_status: Some(vec![-1, 255]),
signal: None,
signal_reason: None,
}),
manual: None,
}),
plugins: vec![],
env: HashMap::new(),
}],
env: HashMap::new(),
agents: HashMap::new(),
notify: vec![],
};
Ok(pipeline)
}
#[derive(Debug, Clone)]
pub struct TeamCityConfig {
pub project_id: String,
pub build_type_id: String,
pub name: String,
pub description: Option<String>,
pub vcs_root: TeamCityVcsRoot,
pub steps: Vec<TeamCityStep>,
pub triggers: Vec<TeamCityTrigger>,
pub parameters: HashMap<String, String>,
pub features: Vec<TeamCityFeature>,
pub failure_conditions: Vec<TeamCityFailureCondition>,
}
#[derive(Debug, Clone)]
pub struct TeamCityVcsRoot {
pub id: String,
pub url: String,
pub default_branch: String,
pub branch_spec: Vec<String>,
pub checkout_policy: String,
}
#[derive(Debug, Clone)]
pub struct TeamCityStep {
pub step_type: TeamCityStepType,
pub name: String,
pub execute_step: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TeamCityStepType {
CommandLine,
MsBuild,
Xcode,
Gradle,
Ant,
NAnt,
Rake,
Fake,
PowerShell,
DotNetCLI,
Docker,
JetBrainsReSharper,
OctopusDeploy,
SMB,
}
#[derive(Debug, Clone)]
pub enum TeamCityTrigger {
VcsTrigger {
branch_filter: String,
quiet_period_seconds: u32,
trigger_rules: Vec<String>,
enable_queue_optimization: bool,
},
ScheduleTrigger {
cron: String,
branch_filter: Option<String>,
},
FinishBuildTrigger {
build_type: String,
branch_filter: Option<String>,
successful_only: bool,
},
RetryTrigger {
retry_delay_seconds: u32,
retry_count: u32,
},
}
#[derive(Debug, Clone)]
pub enum TeamCityFeature {
PullRequests {
vcs_root_ext_id: String,
provider_type: String, },
CommitStatusPublisher {
vcs_root_ext_id: String,
publisher: String, },
XmlReportPlugin {
report_type: String,
rules: Vec<String>,
},
BuildParametersPublishing {
parameters: HashMap<String, String>,
},
SharedResources {
locks: Vec<String>,
},
PerformanceMonitor {
format: String, },
}
#[derive(Debug, Clone)]
pub enum TeamCityFailureCondition {
ErrorMessage {
message: String,
reverse: bool,
},
BuildDuration {
max_duration_seconds: u32,
},
ExitCode {
exit_code: i32,
step_name: Option<String>,
},
MetricChange {
metric_key: String,
units: String,
threshold: f64,
build_type: Option<String>,
},
}
pub fn generate_teamcity_kotlin(
project_name: &str,
cmake_generator: &str,
build_type: &str,
) -> String {
let mut kotlin = String::new();
kotlin.push_str("package _Self.buildTypes\n\n");
kotlin.push_str("import jetbrains.buildServer.configs.kotlin.*\n");
kotlin.push_str("import jetbrains.buildServer.configs.kotlin.buildSteps.script\n\n");
kotlin.push_str(&format!(
r#"
object {} : BuildType({{
id("{}")
name = "{}"
vcs {{
root(DslContext.settingsRoot)
}}
steps {{
script {{
name = "Configure CMake"
scriptContent = """
cmake -B build -G "{}" -DCMAKE_BUILD_TYPE={}
"""
}}
script {{
name = "Build"
scriptContent = """
cmake --build build
"""
}}
script {{
name = "Test"
scriptContent = """
cd build && ctest --output-on-failure
"""
}}
}}
triggers {{
vcs {{
branchFilter = "+:refs/heads/main"
}}
}}
features {{
commitStatusPublisher {{
publisher = github {{
authType = personalToken {{
token = "%github.token%"
}}
}}
}}
}}
}}
"#,
project_name, project_name, project_name,
cmake_generator, build_type
));
kotlin
}
#[derive(Debug, Clone)]
pub struct CiResultReport {
pub ci_system: CiSystem,
pub branch: String,
pub commit_sha: String,
pub total_jobs: usize,
pub successful_jobs: usize,
pub failed_jobs: usize,
pub skipped_jobs: usize,
pub total_duration_secs: f64,
pub test_summary: CiTestSummary,
pub compilation_warnings: usize,
pub compilation_errors: usize,
pub artifacts: Vec<CiArtifact>,
pub badge: Option<CiBadge>,
pub annotations: Vec<CiAnnotation>,
}
impl Default for CiResultReport {
fn default() -> Self {
Self {
ci_system: CiSystem::Custom,
branch: String::new(),
commit_sha: String::new(),
total_jobs: 0,
successful_jobs: 0,
failed_jobs: 0,
skipped_jobs: 0,
total_duration_secs: 0.0,
test_summary: CiTestSummary::default(),
compilation_warnings: 0,
compilation_errors: 0,
artifacts: Vec::new(),
badge: None,
annotations: Vec::new(),
}
}
}
pub fn compile_ci_report(
ci_system: CiSystem,
branch: &str,
commit_sha: &str,
job_results: &[CiJobResult],
) -> CiResultReport {
let mut report = CiResultReport {
ci_system,
branch: branch.to_string(),
commit_sha: commit_sha.to_string(),
..Default::default()
};
let mut total_tests = 0usize;
let mut passed_tests = 0usize;
let mut failed_tests = 0usize;
let mut skipped_tests = 0usize;
let mut total_duration = 0.0f64;
let mut total_warnings = 0usize;
let mut total_errors = 0usize;
for job in job_results {
report.total_jobs += 1;
total_duration += job.duration_secs;
match job.status {
CiJobStatus::Success => report.successful_jobs += 1,
CiJobStatus::Failed | CiJobStatus::TimedOut => report.failed_jobs += 1,
CiJobStatus::Skipped | CiJobStatus::Cancelled => report.skipped_jobs += 1,
_ => {}
}
total_warnings += job.warnings.len();
total_errors += job.errors.len();
if let Some(ref test_summary) = job.test_summary {
total_tests += test_summary.total;
passed_tests += test_summary.passed;
failed_tests += test_summary.failed;
skipped_tests += test_summary.skipped;
}
}
report.total_duration_secs = total_duration;
report.compilation_warnings = total_warnings;
report.compilation_errors = total_errors;
report.test_summary = CiTestSummary {
total: total_tests,
passed: passed_tests,
failed: failed_tests,
skipped: skipped_tests,
errors: 0,
duration_secs: total_duration,
test_suites: Vec::new(),
coverage: None,
};
report
}
#[derive(Debug, Clone)]
pub struct CiBadge {
pub label: String,
pub message: String,
pub color: CiBadgeColor,
pub style: CiBadgeStyle,
pub logo: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiBadgeColor {
BrightGreen,
Green,
YellowGreen,
Yellow,
Orange,
Red,
LightGrey,
Blue,
BlueViolet,
Success,
Important,
Critical,
Informational,
Inactive,
}
impl CiBadgeColor {
pub fn as_hex(&self) -> &'static str {
match self {
Self::BrightGreen => "#44cc11",
Self::Green => "#97ca00",
Self::YellowGreen => "#a4a61d",
Self::Yellow => "#dfb317",
Self::Orange => "#fe7d37",
Self::Red => "#e05d44",
Self::LightGrey => "#9f9f9f",
Self::Blue => "#007ec6",
Self::BlueViolet => "#8b00ff",
Self::Success => "#4c1",
Self::Important => "#e05d44",
Self::Critical => "#e05d44",
Self::Informational => "#007ec6",
Self::Inactive => "#9f9f9f",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiBadgeStyle {
Plastic,
Flat,
FlatSquare,
ForTheBadge,
Social,
}
impl CiBadgeStyle {
pub fn as_str(&self) -> &'static str {
match self {
Self::Plastic => "plastic",
Self::Flat => "flat",
Self::FlatSquare => "flat-square",
Self::ForTheBadge => "for-the-badge",
Self::Social => "social",
}
}
}
pub fn generate_badge_url(badge: &CiBadge) -> String {
let color_hex = badge.color.as_hex().trim_start_matches('#');
let mut url = format!(
"https://img.shields.io/badge/{}-{}-{}",
urlencoding(&badge.label),
urlencoding(&badge.message),
color_hex
);
url.push_str(&format!("?style={}", badge.style.as_str()));
if let Some(ref logo) = badge.logo {
url.push_str(&format!("&logo={}", logo));
}
url
}
pub fn generate_build_status_badge(status: CiJobStatus) -> CiBadge {
let (message, color) = match status {
CiJobStatus::Success => ("passing", CiBadgeColor::BrightGreen),
CiJobStatus::Failed | CiJobStatus::TimedOut => ("failing", CiBadgeColor::Red),
CiJobStatus::Running => ("running", CiBadgeColor::Blue),
CiJobStatus::Pending => ("pending", CiBadgeColor::Orange),
CiJobStatus::Skipped | CiJobStatus::Cancelled => ("skipped", CiBadgeColor::LightGrey),
_ => ("unknown", CiBadgeColor::LightGrey),
};
CiBadge {
label: "build".to_string(),
message: message.to_string(),
color,
style: CiBadgeStyle::Flat,
logo: Some("github".to_string()),
url: None,
}
}
pub fn generate_coverage_badge(coverage_pct: f64) -> CiBadge {
let color = if coverage_pct >= 90.0 {
CiBadgeColor::BrightGreen
} else if coverage_pct >= 75.0 {
CiBadgeColor::Green
} else if coverage_pct >= 60.0 {
CiBadgeColor::YellowGreen
} else if coverage_pct >= 40.0 {
CiBadgeColor::Yellow
} else {
CiBadgeColor::Red
};
CiBadge {
label: "coverage".to_string(),
message: format!("{:.1}%", coverage_pct),
color,
style: CiBadgeStyle::Flat,
logo: Some("codecov".to_string()),
url: None,
}
}
#[derive(Debug, Clone)]
pub struct CiAnnotation {
pub path: String,
pub start_line: u32,
pub end_line: u32,
pub start_column: Option<u32>,
pub end_column: Option<u32>,
pub annotation_level: CiAnnotationLevel,
pub message: String,
pub title: Option<String>,
pub raw_details: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CiAnnotationLevel {
Notice,
Warning,
Failure,
}
impl CiAnnotationLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Notice => "notice",
Self::Warning => "warning",
Self::Failure => "failure",
}
}
}
pub fn generate_annotations_from_compiler_output(
output: &str,
level: CiAnnotationLevel,
) -> Vec<CiAnnotation> {
let mut annotations = Vec::new();
for line in output.lines() {
if let Some(annotation) = parse_compiler_line_to_annotation(line, level) {
annotations.push(annotation);
}
}
annotations
}
fn parse_compiler_line_to_annotation(line: &str, default_level: CiAnnotationLevel) -> Option<CiAnnotation> {
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() < 4 {
return None;
}
let path = parts[0];
let start_line: u32 = parts[1].parse().ok()?;
let start_column: u32 = parts[2].parse().ok()?;
let (level, message) = determine_severity(parts[3]);
Some(CiAnnotation {
path: path.to_string(),
start_line,
end_line: start_line,
start_column: Some(start_column),
end_column: None,
annotation_level: level,
message: message.to_string(),
title: None,
raw_details: None,
})
}
fn determine_severity(rest: &str) -> (CiAnnotationLevel, String) {
let trimmed = rest.trim();
if trimmed.starts_with("error:") {
(CiAnnotationLevel::Failure, trimmed.to_string())
} else if trimmed.starts_with("warning:") {
(CiAnnotationLevel::Warning, trimmed.to_string())
} else if trimmed.starts_with("note:") {
(CiAnnotationLevel::Notice, trimmed.to_string())
} else {
(CiAnnotationLevel::Notice, trimmed.to_string())
}
}
fn urlencoding(s: &str) -> String {
s.replace('-', "--")
.replace('_', "__")
.replace(' ', "_")
.replace('/', "%2F")
.replace('\\', "%5C")
.replace('?', "%3F")
.replace('#', "%23")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ci_system_names() {
assert_eq!(CiSystem::GitHubActions.name(), "GitHub Actions");
assert_eq!(CiSystem::GitLabCI.name(), "GitLab CI");
assert_eq!(CiSystem::Jenkins.name(), "Jenkins");
}
#[test]
fn test_ci_job_status() {
assert!(CiJobStatus::Success.is_success());
assert!(CiJobStatus::Success.is_terminal());
assert!(CiJobStatus::Failed.is_terminal());
assert!(!CiJobStatus::Running.is_terminal());
}
#[test]
fn test_parse_github_workflow() {
let yaml_content = r#"
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cmake -B build && cmake --build build
"#;
let tmp = std::env::temp_dir().join("test_workflow.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let workflow = parse_github_workflow(&tmp);
assert!(workflow.is_ok());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_generate_github_workflow() {
let yaml = generate_github_workflow(
"CI Test",
&["ubuntu-latest"],
&["clang", "gcc"],
&["Debug", "Release"],
&["-DENABLE_TESTS=ON"],
Some("cd build && ctest"),
);
assert!(yaml.contains("name: CI Test"));
assert!(yaml.contains("ubuntu-latest"));
assert!(yaml.contains("actions/checkout@v4"));
assert!(yaml.contains("ctest"));
}
#[test]
fn test_parse_gitlab_ci() {
let yaml_content = r#"
stages:
- build
- test
build-job:
stage: build
script:
- cmake -B build
- cmake --build build
"#;
let tmp = std::env::temp_dir().join("test_gitlab.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let pipeline = parse_gitlab_ci(&tmp);
assert!(pipeline.is_ok());
assert_eq!(pipeline.unwrap().stages.len(), 3);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_generate_gitlab_ci() {
let yaml = generate_gitlab_ci(
"gcc:13",
&["build", "test", "deploy"],
&[
"-DCMAKE_BUILD_TYPE=Release",
"-DENABLE_TESTS=ON",
],
4,
);
assert!(yaml.contains("gcc:13"));
assert!(yaml.contains("build"));
assert!(yaml.contains("test"));
assert!(yaml.contains("parallel: 4"));
}
#[test]
fn test_parse_jenkinsfile() {
let jf_content = r#"
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'cmake -B build && cmake --build build'
}
}
}
}
"#;
let tmp = std::env::temp_dir().join("test_Jenkinsfile");
std::fs::write(&tmp, jf_content).unwrap();
let pipeline = parse_jenkinsfile(&tmp);
assert!(pipeline.is_ok());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_generate_jenkinsfile() {
let jf = generate_jenkinsfile("gcc:13", "Ninja", "Release", 4);
assert!(jf.contains("pipeline {"));
assert!(jf.contains("docker { image 'gcc:13' }"));
assert!(jf.contains("cmake -B build"));
assert!(jf.contains("ctest -j4"));
}
#[test]
fn test_ci_test_summary_default() {
let summary = CiTestSummary::default();
assert_eq!(summary.total, 0);
assert_eq!(summary.passed, 0);
}
#[test]
fn test_badge_generation() {
let badge = generate_build_status_badge(CiJobStatus::Success);
assert_eq!(badge.label, "build");
assert_eq!(badge.message, "passing");
let url = generate_badge_url(&badge);
assert!(url.contains("shields.io"));
assert!(url.contains("passing"));
}
#[test]
fn test_coverage_badge() {
let badge = generate_coverage_badge(92.5);
assert_eq!(badge.message, "92.5%");
assert_eq!(badge.color.as_hex(), "#44cc11");
}
#[test]
fn test_parse_compiler_line_to_annotation() {
let line = "src/main.cpp:42:5: error: expected ';' after expression";
let annotation = parse_compiler_line_to_annotation(line, CiAnnotationLevel::Failure);
assert!(annotation.is_some());
let ann = annotation.unwrap();
assert_eq!(ann.path, "src/main.cpp");
assert_eq!(ann.start_line, 42);
assert_eq!(ann.start_column, Some(5));
assert_eq!(ann.annotation_level, CiAnnotationLevel::Failure);
}
#[test]
fn test_ci_result_report() {
let job_results = vec![
CiJobResult {
job_name: "build-linux".to_string(),
stage: "build".to_string(),
status: CiJobStatus::Success,
exit_code: Some(0),
duration_secs: 120.0,
started_at: None,
finished_at: None,
artifacts: vec![],
logs_url: None,
test_summary: Some(CiTestSummary {
total: 50,
passed: 49,
failed: 1,
skipped: 0,
errors: 0,
duration_secs: 30.0,
test_suites: vec![],
coverage: Some(85.0),
}),
compiler_output: None,
warnings: vec![
CiWarning {
message: "unused variable".to_string(),
file: Some("test.cpp".to_string()),
line: Some(10),
category: None,
},
],
errors: vec![],
retry_count: 0,
},
CiJobResult {
job_name: "build-macos".to_string(),
stage: "build".to_string(),
status: CiJobStatus::Success,
exit_code: Some(0),
duration_secs: 150.0,
started_at: None,
finished_at: None,
artifacts: vec![],
logs_url: None,
test_summary: Some(CiTestSummary::default()),
compiler_output: None,
warnings: vec![],
errors: vec![],
retry_count: 0,
},
];
let report = compile_ci_report(
CiSystem::GitHubActions,
"main",
"abc123",
&job_results,
);
assert_eq!(report.total_jobs, 2);
assert_eq!(report.successful_jobs, 2);
assert_eq!(report.failed_jobs, 0);
assert_eq!(report.test_summary.total, 50);
assert_eq!(report.compilation_warnings, 1);
}
#[test]
fn test_drone_pipeline_parse() {
let yaml_content = r#"
kind: pipeline
name: default
platform:
os: linux
arch: amd64
steps:
- name: build
image: gcc:13
commands:
- cmake -B build
- cmake --build build
"#;
let tmp = std::env::temp_dir().join("test_drone.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let pipeline = parse_drone_yml(&tmp);
assert!(pipeline.is_ok());
let p = pipeline.unwrap();
assert_eq!(p.kind, "pipeline");
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_buildkite_pipeline_parse() {
let yaml_content = r#"
steps:
- label: "Build"
command: "cmake -B build && cmake --build build"
agents:
queue: "default"
"#;
let tmp = std::env::temp_dir().join("test_buildkite.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let pipeline = parse_buildkite_pipeline(&tmp);
assert!(pipeline.is_ok());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_teamcity_kotlin_generation() {
let kotlin = generate_teamcity_kotlin("MyBuild", "Ninja", "Release");
assert!(kotlin.contains("MyBuild"));
assert!(kotlin.contains("Ninja"));
assert!(kotlin.contains("Release"));
assert!(kotlin.contains("cmake -B build"));
assert!(kotlin.contains("ctest --output-on-failure"));
}
#[test]
fn test_azure_pipeline_parse() {
let yaml_content = r#"
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- script: cmake -B build && cmake --build build
displayName: Build
"#;
let tmp = std::env::temp_dir().join("test_azure.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let config = parse_azure_pipelines(&tmp);
assert!(config.is_ok());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_travis_yml_parse() {
let yaml_content = r#"
language: cpp
compiler:
- clang
- gcc
script:
- cmake -B build && cmake --build build
"#;
let tmp = std::env::temp_dir().join("test_travis.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let config = parse_travis_yml(&tmp);
assert!(config.is_ok());
assert_eq!(config.unwrap().language, "cpp");
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_circleci_config_parse() {
let yaml_content = r#"
version: 2.1
jobs:
build:
docker:
- image: gcc:13
steps:
- checkout
- run: cmake -B build && cmake --build build
"#;
let tmp = std::env::temp_dir().join("test_circleci.yml");
std::fs::write(&tmp, yaml_content).unwrap();
let config = parse_circleci_config(&tmp);
assert!(config.is_ok());
std::fs::remove_file(&tmp).ok();
}
}