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}