depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! GitLab CI/CD module.

use colored::Colorize;

use crate::entities::environment::RunEnvironment;
use crate::entities::gitlab_cicd_opts::GitLabOpts;
use crate::pipelines::DescribedPipeline;
use crate::project::DeployerProjectOptions;

const GL_CICD_TEMPLATE: &str = r#"# Generated by Deployer 2.X
variables:
  TERM: xterm
{cache-rules}
{pipeline-title}:{base-image}{preflight-cmds}{artifacts}{trigger-rules}
  stage: deployer
  script:
    - chmod +x .gitlab/workflows/{shell-script-path}
    - sh ./.gitlab/workflows/{shell-script-path}
"#;

/// Export given pipeline to GitLab CI/CD configuration.
pub fn export(
  config: &DeployerProjectOptions,
  pipeline: &DescribedPipeline,
  gl_opts: Option<&GitLabOpts>,
  env: &RunEnvironment<'_>,
  shell_script: &str,
  output_dir: Option<&str>,
) -> anyhow::Result<()> {
  let gl_dir = if let Some(output) = output_dir {
    let dir = std::path::PathBuf::from(output);
    let _ = std::fs::create_dir_all(dir.as_path());
    dir
  } else {
    let dir = env
      .project_dir
      .ok_or(anyhow::anyhow!("Can't get project dir!"))?
      .join(".gitlab")
      .join("workflows");
    let _ = std::fs::create_dir_all(dir.as_path());
    dir
  };

  let pipe_filename = format!(".pipe.{}.sh", env.master_pipeline);
  std::fs::write(gl_dir.join(pipe_filename.as_str()), shell_script)?;

  let trigger_rules = if gl_opts.is_none_or(|gl_opts| gl_opts.rules.is_empty()) {
    "\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH".to_string()
  } else if gl_opts.unwrap().rules.is_empty() {
    "".to_string()
  } else {
    format!(
      "\n  rules:{}",
      gl_opts
        .unwrap()
        .rules
        .iter()
        .map(|r| format!("\n    - if: {r}"))
        .collect::<Vec<_>>()
        .join(""),
    )
  };

  let artifacts_actions = if !pipeline.artifacts.is_empty() {
    format!(
      "\n  artifacts:\n    paths:{}",
      pipeline
        .artifacts
        .iter()
        .map(|a| format!("\n      - {}", a.from))
        .collect::<Vec<_>>()
        .join("")
    )
  } else {
    String::new()
  };

  let preflight_cmds = if let Some(gl_opts) = gl_opts
    && !gl_opts.preflight_cmds.is_empty()
  {
    format!(
      "\n  before_script:\n    - |{}",
      gl_opts
        .preflight_cmds
        .iter()
        .map(|s| format!("\n      {s}"))
        .collect::<Vec<_>>()
        .join("")
    )
  } else {
    String::new()
  };

  let cache_rules = if gl_opts.is_none_or(|gl_opts| gl_opts.enable_cache) && !config.cache_files.is_empty() {
    let mut cache_files: std::collections::BTreeSet<_> = config.cache_files.iter().collect();
    cache_files.remove(&std::path::PathBuf::from(".git"));
    let cache_files = cache_files
      .iter()
      .map(|cf| {
        format!(
          "\n    - {}",
          cf.to_str().expect("`cache_file` contains non-UTF8 symbols!")
        )
      })
      .collect::<Vec<_>>()
      .join("");
    format!("\ncache:\n  key: \"${{CI_COMMIT_REF_SLUG}}\"\n  paths:{cache_files}\n")
  } else {
    String::new()
  };

  let cicd_script = GL_CICD_TEMPLATE
    .replace("{pipeline-title}", env.master_pipeline)
    .replace("{shell-script-path}", pipe_filename.as_str())
    .replace("{trigger-rules}", trigger_rules.as_str())
    .replace(
      "{base-image}",
      &format!(
        "\n  image: {}",
        gl_opts
          .map(|gl_opts| gl_opts.base_image.as_deref().unwrap_or("ubuntu:latest"))
          .unwrap_or("ubuntu:latest")
      ),
    )
    .replace("{artifacts}", artifacts_actions.as_str())
    .replace("{preflight-cmds}", preflight_cmds.as_str())
    .replace("{cache-rules}", cache_rules.as_str());

  let cicd_filename = format!("{}.yml", env.master_pipeline);
  std::fs::write(gl_dir.join(cicd_filename.as_str()), cicd_script.as_str())?;

  println!(
    "{}",
    "Don't forget to add the following lines to `.gitlab-ci.yml`:".red()
  );
  println!(
    "{}",
    format!("include:\n  - local: '.gitlab/workflows/{cicd_filename}'\n\nstages:\n  - deployer")
      .as_str()
      .blue()
  );

  Ok(())
}