coodev-runner 0.1.42

A simple runner for coodev
Documentation
use crate::{
  actions::{Action, ActionSteps},
  constants::COODEV_CONTAINER_IMAGE,
  error,
  user_config::{UserActionStep, UserCommandStep, UserStep},
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct GitCheckoutOptions {
  /// The repository to checkout. For example: `panghu-huang/github-api`
  pub repository: Option<String>,
  /// The ref to checkout. If you want to checkout a pull request, you need to provide a ref like `refs/pull/1/merge`
  #[serde(rename = "ref")]
  pub ref_name: Option<String>,
  /// If you want clone private repository, you need to provide a token within `github-server-url`
  /// For example: `https://x-access-token:{GITHUB_TOKEN}@github.com`
  #[serde(rename = "github-server-url")]
  pub github_server_url: Option<String>,
  /// The relative path to checkout. Default is `.`.
  pub path: Option<String>,
  /// The depth of git clone. Default is empty.
  pub depth: Option<String>,
}

#[derive(Debug, Clone)]
pub struct GitCheckoutAction;

impl GitCheckoutAction {
  pub fn new() -> Self {
    Self {}
  }
}

impl Action for GitCheckoutAction {
  fn normalize(&self, step: UserActionStep) -> crate::Result<ActionSteps> {
    let with = step.with.clone();

    let git_checkout_options = match with {
      Some(with) => serde_yaml::from_value::<GitCheckoutOptions>(with).map_err(|_| {
        error::Error::workflow_config_error(
          "Optional `with` options for `git/checkout` action is not valid.",
        )
      })?,
      None => GitCheckoutOptions::default(),
    };

    let github_server_url = git_checkout_options
      .github_server_url
      .clone()
      .unwrap_or("".to_string());

    let ref_name = git_checkout_options
      .ref_name
      .clone()
      .unwrap_or("".to_string());

    let repository = git_checkout_options
      .repository
      .clone()
      .unwrap_or("".to_string());

    let path = git_checkout_options.path.clone().unwrap_or(".".to_string());

    let depth = git_checkout_options.depth.clone().unwrap_or("".to_string());

    let prepare = format!(
      "
      set -e
      echo \"$(git --version)\"
      # User arguments
      github_server_url={github_server_url}
      ref={ref_name}
      repository={repository}
      path={path}
      depth={depth}
    "
    );

    let run = r#"
      # Format arguments
      default_server_url="https://oauth2:${COODEV_ACCESS_TOKEN}@github.com"
      github_server_url=${github_server_url:-$default_server_url}
      repository=${repository:-$COODEV_REPOSITORY}
      ref=${ref:-$COODEV_REF}

      # config default branch
      git config --global init.defaultBranch $COODEV_DEFAULT_BRANCH
      
      # colorize output of git commands
      git config --global color.ui always

      echo "Checking out $ref from $repository..."
      # Create path if not exists
      if [ ! -d $path ]; then
        mkdir -p $path
      fi

      cd $path
      git init .
      git remote add origin $github_server_url/$repository.git

      git fetch \
        $([ -n "$depth" ] && echo "--depth $depth" || echo "") \
        origin $ref

      git switch -c $ref FETCH_HEAD

      echo "Checkout done. Current commit:"
      git log -1
    "#;

    let name = step.name.clone().unwrap_or("git/checkout".to_string());
    let continue_on_error = step.continue_on_error.unwrap_or(true);
    let timeout = step.timeout.unwrap_or("10m".to_string());

    Ok(ActionSteps {
      pre: None,
      run: UserStep::Command(UserCommandStep {
        image: Some(COODEV_CONTAINER_IMAGE.to_string()),
        name: Some(name),
        run: format!("{}{}", prepare, run),
        continue_on_error: Some(continue_on_error),
        environments: None,
        secrets: None,
        volumes: None,
        security_opts: None,
        timeout: Some(timeout),
      }),
      post: None,
    })
  }
}

#[cfg(test)]
mod tests {
  use crate::{
    utils::test::{create_runner, run_workflow},
    CreateWorkflowOptions, WorkflowAPIEvent, WorkflowEvent, WorkflowRunOptions,
  };

  #[tokio::test]
  #[ignore]
  async fn test_git_checkout_action() -> anyhow::Result<()> {
    let yaml = r#"
jobs:
  test-job:
    name: Test clone repository
    steps:
      - name: Checkout
        uses: git/checkout
        with:
          repository: panghu-huang/octocrate
          ref: refs/heads/main
    "#;

    let _res = run_workflow(yaml).await?;

    Ok(())
  }

  #[tokio::test]
  #[ignore]
  async fn test_checkout_private_repo() -> anyhow::Result<()> {
    let runner = create_runner()?;

    let yaml = r#"
jobs:
  test-job:
    name: Test clone repository
    steps:
      - name: Checkout
        uses: git/checkout
    "#;
    let workflow_runner = runner.create_workflow_runner(CreateWorkflowOptions {
      config: yaml.to_string(),
      event: WorkflowEvent::API(WorkflowAPIEvent {
        repo_owner: "coodevjs".to_string(),
        repo_name: "coodev-runner".to_string(),
        ref_name: "main".to_string(),
        sha: "4c03973fa72b114984c9b6eb14a3b4fb415f3877".to_string(),
        pr_number: None,
      }),
    })?;

    let _res = workflow_runner
      .run(WorkflowRunOptions {
        run_id: "runner".to_string(),
        environments: None,
      })
      .await?;

    Ok(())
  }
}