workflow_checks.rs

  1mod check_run_patterns;
  2
  3use std::{fs, path::PathBuf};
  4
  5use annotate_snippets::Renderer;
  6use anyhow::{Result, anyhow};
  7use clap::Parser;
  8use itertools::{Either, Itertools};
  9use serde_yaml::Value;
 10use strum::IntoEnumIterator;
 11
 12use crate::tasks::{
 13    workflow_checks::check_run_patterns::{
 14        RunValidationError, WorkflowFile, WorkflowValidationError,
 15    },
 16    workflows::WorkflowType,
 17};
 18
 19pub use check_run_patterns::validate_run_command;
 20
 21#[derive(Default, Parser)]
 22pub struct WorkflowValidationArgs {}
 23
 24pub fn validate(_: WorkflowValidationArgs) -> Result<()> {
 25    let (parsing_errors, file_errors): (Vec<_>, Vec<_>) = get_all_workflow_files()
 26        .map(check_workflow)
 27        .flat_map(Result::err)
 28        .partition_map(|error| match error {
 29            WorkflowError::ParseError(error) => Either::Left(error),
 30            WorkflowError::ValidationError(error) => Either::Right(error),
 31        });
 32
 33    if !parsing_errors.is_empty() {
 34        Err(anyhow!(
 35            "Failed to read or parse some workflow files: {}",
 36            parsing_errors.into_iter().join("\n")
 37        ))
 38    } else if !file_errors.is_empty() {
 39        let errors: Vec<_> = file_errors
 40            .iter()
 41            .map(|error| error.annotation_group())
 42            .collect();
 43
 44        let renderer =
 45            Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Ascii);
 46        println!("{}", renderer.render(errors.as_slice()));
 47
 48        Err(anyhow!("Workflow checks failed!"))
 49    } else {
 50        Ok(())
 51    }
 52}
 53
 54enum WorkflowError {
 55    ParseError(anyhow::Error),
 56    ValidationError(Box<WorkflowValidationError>),
 57}
 58
 59fn get_all_workflow_files() -> impl Iterator<Item = PathBuf> {
 60    WorkflowType::iter()
 61        .map(|workflow_type| workflow_type.folder_path())
 62        .flat_map(|folder_path| {
 63            fs::read_dir(folder_path).into_iter().flat_map(|entries| {
 64                entries
 65                    .flat_map(Result::ok)
 66                    .map(|entry| entry.path())
 67                    .filter(|path| {
 68                        path.extension()
 69                            .is_some_and(|ext| ext == "yaml" || ext == "yml")
 70                    })
 71            })
 72        })
 73}
 74
 75fn check_workflow(workflow_file_path: PathBuf) -> Result<(), WorkflowError> {
 76    fn collect_errors(
 77        iter: impl Iterator<Item = Result<(), Vec<RunValidationError>>>,
 78    ) -> Result<(), Vec<RunValidationError>> {
 79        Some(iter.flat_map(Result::err).flatten().collect::<Vec<_>>())
 80            .filter(|errors| !errors.is_empty())
 81            .map_or(Ok(()), Err)
 82    }
 83
 84    fn check_recursive(key: &Value, value: &Value) -> Result<(), Vec<RunValidationError>> {
 85        match value {
 86            Value::Mapping(mapping) => collect_errors(
 87                mapping
 88                    .into_iter()
 89                    .map(|(key, value)| check_recursive(key, value)),
 90            ),
 91            Value::Sequence(sequence) => collect_errors(
 92                sequence
 93                    .into_iter()
 94                    .map(|value| check_recursive(key, value)),
 95            ),
 96            Value::String(string) => check_string(key, string).map_err(|error| vec![error]),
 97            Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Ok(()),
 98        }
 99    }
100
101    let file_content =
102        WorkflowFile::load(&workflow_file_path).map_err(WorkflowError::ParseError)?;
103
104    check_recursive(&Value::Null, &file_content.parsed_content).map_err(|errors| {
105        WorkflowError::ValidationError(Box::new(WorkflowValidationError::new(
106            errors,
107            file_content,
108            workflow_file_path,
109        )))
110    })
111}
112
113fn check_string(key: &Value, value: &str) -> Result<(), RunValidationError> {
114    match key {
115        Value::String(key) if key == "run" => validate_run_command(value),
116        _ => Ok(()),
117    }
118}