depl 2.4.3

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

use colored::Colorize;
use serde::{Deserialize, Serialize};

/// GitHub CI/CD options.
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct GitHubOpts {
  /// Trigger pipeline on push into specified branches.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub on_push_branches: Vec<String>,
  /// Trigger pipeline on pull requests into specified branches.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub on_pull_requests_branches: Vec<String>,
  /// Base image (default is `ubuntu-latest`).
  #[serde(skip_serializing_if = "Option::is_none")]
  pub base_image: Option<String>,
  /// Preflight steps.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub preflight_steps: Vec<serde_pretty_yaml::Value>,
  /// Enable cache.
  ///
  /// Enabled by default.
  #[serde(default = "crate::utils::yes", skip_serializing_if = "crate::utils::is_true")]
  pub enable_cache: bool,
}

impl Ord for GitHubOpts {
  fn cmp(&self, other: &Self) -> std::cmp::Ordering {
    self.base_image.cmp(&other.base_image)
  }
}

#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for GitHubOpts {
  fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
    Some(self.cmp(other))
  }
}

impl GitHubOpts {
  /// Returns compiled preflight steps.
  pub fn compile_preflight(&self) -> anyhow::Result<String> {
    let preflight = serde_pretty_yaml::to_string_pretty(&self.preflight_steps)?
      .trim_end()
      .split('\n')
      .map(|line| format!("      {line}"))
      .collect::<Vec<_>>()
      .join("\n");
    Ok(preflight)
  }

  /// Creates GitHub Actions options from prompt.
  pub fn new_from_prompt() -> anyhow::Result<Self> {
    let on_push_branches = crate::utils::tags_custom_type(
      "Enter branches to trigger on push (comma-separated, or leave empty):",
      None,
    )
    .prompt()?;

    let on_pull_requests_branches = crate::utils::tags_custom_type(
      "Enter branches to trigger on pull requests (comma-separated, or leave empty):",
      None,
    )
    .prompt()?;

    let base_image = inquire_reorder::Text::new("Enter GitHub Actions runner image:")
      .with_initial_value("ubuntu-latest")
      .prompt_skippable()?;
    let base_image = base_image.filter(|v| v != "ubuntu-latest");

    let enable_cache = inquire_reorder::Confirm::new("Enable caching?")
      .with_default(true)
      .prompt()?;

    Ok(Self {
      on_push_branches,
      on_pull_requests_branches,
      base_image,
      preflight_steps: vec![],
      enable_cache,
    })
  }

  /// Edits GitHub Actions options from prompt.
  pub async fn edit_from_prompt(&mut self) -> anyhow::Result<()> {
    loop {
      println!(
        "Current GitHub Actions config: runner={}, push branches={:?}, PR branches={:?}, cache={}.",
        self.base_image.as_deref().unwrap_or("ubuntu-latest").green(),
        self.on_push_branches,
        self.on_pull_requests_branches,
        if self.enable_cache {
          "enabled".green()
        } else {
          "disabled".green()
        },
      );
      let opts = vec![
        "Edit push trigger branches",
        "Edit pull request trigger branches",
        "Edit runner image",
        "Edit preflight steps",
        "Toggle cache",
      ];
      let select = inquire_reorder::Select::new("Select action (hit `esc` when done):", opts).prompt_skippable()?;
      let Some(select) = select else { return Ok(()) };
      match select {
        "Edit push trigger branches" => {
          let joined = self.on_push_branches.join(", ");
          self.on_push_branches = crate::utils::tags_custom_type(
            "Enter branches to trigger on push (comma-separated):",
            if joined.is_empty() { None } else { Some(joined.as_str()) },
          )
          .prompt()?;
        }
        "Edit pull request trigger branches" => {
          let joined = self.on_pull_requests_branches.join(", ");
          self.on_pull_requests_branches = crate::utils::tags_custom_type(
            "Enter branches to trigger on pull requests (comma-separated):",
            if joined.is_empty() { None } else { Some(joined.as_str()) },
          )
          .prompt()?;
        }
        "Edit runner image" => {
          let image = inquire_reorder::Text::new("Enter GitHub Actions runner image:")
            .with_initial_value(self.base_image.as_deref().unwrap_or("ubuntu-latest"))
            .prompt_skippable()?;
          self.base_image = image.filter(|v| v != "ubuntu-latest");
        }
        "Edit preflight steps" => {
          let current = if self.preflight_steps.is_empty() {
            String::new()
          } else {
            serde_pretty_yaml::to_string_pretty(&self.preflight_steps)
              .unwrap_or_default()
              .trim()
              .to_string()
          };
          let new_steps = crate::tui::utils::edit_with_nano(
            "Edit preflight steps in GitHub Actions YAML format.\nEach step should be a YAML mapping with `name`, `uses`, `run`, etc.\nWrite steps below this comment.",
            current,
            Some("yaml"),
          )
          .await?;
          if new_steps.is_empty() {
            self.preflight_steps = vec![];
          } else {
            match serde_pretty_yaml::from_str::<Vec<serde_pretty_yaml::Value>>(&new_steps) {
              Ok(steps) => self.preflight_steps = steps,
              Err(e) => println!("Failed to parse YAML steps: {e}. Keeping previous value."),
            }
          }
        }
        "Toggle cache" => {
          self.enable_cache = inquire_reorder::Confirm::new("Enable caching?")
            .with_default(self.enable_cache)
            .prompt()?;
        }
        _ => unreachable!(),
      }
    }
  }
}