depl 2.4.3

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

use crate::entities::environment::RunEnvironment;
use crate::entities::github_cicd_opts::GitHubOpts;
use crate::pipelines::DescribedPipeline;
use crate::project::DeployerProjectOptions;

const TRIGGERS: &str = r#"
on:
{on-push}
{on-pull}
"#;

const GH_CICD_TEMPLATE: &str = r#"# Generated by Deployer 2.X
name: {pipeline-title}
{triggers}
env:
  TERM: xterm

jobs:
  {pipeline-title}:
    name: {pipeline-title}
    runs-on: {base-image}
    steps:
      - uses: actions/checkout@v4
{preflight-steps}{restore-cache}
      - name: Run pipeline
        run: |
          chmod +x .github/workflows/{shell-script-path}
          ./.github/workflows/{shell-script-path}
{save-cache}{artifacts-step}
"#;

const GH_CICD_DEPL_TEMPLATE: &str = r#"# Generated by Deployer 2.X
name: {pipeline-title}
{triggers}
env:
  TERM: xterm

jobs:
  {pipeline-title}:
    name: {pipeline-title}
    runs-on: {base-image}
    steps:
      - uses: impulse-sw/deployer-action@0.6
      - uses: actions/checkout@v4
{preflight-steps}{restore-cache}
      - name: Run pipeline
        run: |
          depl run {pipeline-title} --current --no-clear
{save-cache}{artifacts-step}
"#;

/// Export given pipeline to GitHub CI/CD configuration.
pub fn export(
  config: &DeployerProjectOptions,
  pipeline: &DescribedPipeline,
  gh_opts: Option<&GitHubOpts>,
  env: &RunEnvironment<'_>,
  shell_script: &str,
  output_dir: Option<&str>,
) -> anyhow::Result<()> {
  let gh_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(".github")
      .join("workflows");
    let _ = std::fs::create_dir_all(dir.as_path());
    dir
  };

  let triggers = if gh_opts
    .is_none_or(|gh_opts| gh_opts.on_push_branches.is_empty() && gh_opts.on_pull_requests_branches.is_empty())
  {
    String::new()
  } else {
    let mut triggers = TRIGGERS.to_string();
    let push = &gh_opts.unwrap().on_push_branches;
    let pull = &gh_opts.unwrap().on_pull_requests_branches;
    if !push.is_empty() {
      triggers = triggers.replace(
        "{on-push}",
        &format!(
          "  push:\n    branches:{}",
          push
            .iter()
            .map(|b| format!("\n      - {b}"))
            .collect::<Vec<_>>()
            .join("")
        ),
      );
    }
    if !pull.is_empty() {
      triggers = triggers.replace(
        "{on-pull}",
        &format!(
          "  pull_request:\n    branches:{}",
          pull
            .iter()
            .map(|b| format!("\n      - {b}"))
            .collect::<Vec<_>>()
            .join("")
        ),
      );
    }
    triggers
  };

  let artifacts_actions = if !pipeline.artifacts.is_empty() {
    format!(
      "\n      - uses: actions/upload-artifact@v4\n        with:\n          name: {}\n          path: |{}",
      env.master_pipeline,
      pipeline
        .artifacts
        .iter()
        .map(|a| format!("\n            {}", a.from))
        .collect::<Vec<_>>()
        .join("")
    )
  } else {
    String::new()
  };

  let preflight_steps = if let Some(gh_opts) = gh_opts
    && !gh_opts.preflight_steps.is_empty()
  {
    format!("\n      # Preflight\n{}\n", gh_opts.compile_preflight()?)
  } else {
    String::new()
  };

  let (restore_cache, save_cache) = if gh_opts.is_none_or(|gh_opts| gh_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("");
    let restore_cache = format!(
      "\n      - name: Extract branch name\n        shell: bash\n        run: echo \"branch=${{GITHUB_HEAD_REF:-${{GITHUB_REF#refs/heads/}}}}\" >> $GITHUB_OUTPUT\n        id: extract_branch\n\n      - name: Restore cache\n        id: cache-restore\n        uses: actions/cache/restore@v4\n        with:\n          path: |{cache_files}\n          key: ${{{{ steps.extract_branch.outputs.branch }}}}-${{{{ runner.os }}}}-{}\n",
      env.master_pipeline
    );
    let save_cache = format!(
      "\n      - name: Save cache\n        id: cache-save\n        uses: actions/cache/save@v4\n        with:\n          path: |{cache_files}\n          key: ${{{{ steps.cache-restore.outputs.cache-primary-key }}}}\n"
    );
    (restore_cache, save_cache)
  } else {
    (String::new(), String::new())
  };

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

    let cicd_script = GH_CICD_TEMPLATE
      .replace("{pipeline-title}", env.master_pipeline)
      .replace("{shell-script-path}", pipe_filename.as_str())
      .replace("{triggers}", triggers.as_str())
      .replace(
        "{base-image}",
        gh_opts
          .map(|gh_opts| gh_opts.base_image.as_deref().unwrap_or("ubuntu-latest"))
          .unwrap_or("ubuntu-latest"),
      )
      .replace("{artifacts-step}", artifacts_actions.as_str())
      .replace("{preflight-steps}", preflight_steps.as_str())
      .replace("{restore-cache}", restore_cache.as_str())
      .replace("{save-cache}", save_cache.as_str());

    std::fs::write(
      gh_dir.join(format!("{}.yml", env.master_pipeline)),
      cicd_script.as_str(),
    )?;
  } else {
    let cicd_script = GH_CICD_DEPL_TEMPLATE
      .replace("{pipeline-title}", env.master_pipeline)
      .replace("{triggers}", triggers.as_str())
      .replace(
        "{base-image}",
        gh_opts
          .map(|gh_opts| gh_opts.base_image.as_deref().unwrap_or("ubuntu-latest"))
          .unwrap_or("ubuntu-latest"),
      )
      .replace("{artifacts-step}", artifacts_actions.as_str())
      .replace("{preflight-steps}", preflight_steps.as_str())
      .replace("{restore-cache}", restore_cache.as_str())
      .replace("{save-cache}", save_cache.as_str());

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

  Ok(())
}