mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-02-24 03:49:45 +01:00
workflow validator
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
336
Cargo.lock
generated
Normal file
336
Cargo.lock
generated
Normal file
@@ -0,0 +1,336 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wrkflw"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
]
|
||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "wrkflw"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "GitHub Workflow evaluator for validating workflow files"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
colored = "2.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
61
src/evaluator.rs
Normal file
61
src/evaluator.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use colored::*;
|
||||
use serde_yaml::{self, Value};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::models::ValidationResult;
|
||||
use crate::validators::{validate_jobs, validate_triggers};
|
||||
|
||||
pub fn evaluate_workflow_file(path: &Path, verbose: bool) -> Result<ValidationResult, String> {
|
||||
let content = fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
|
||||
// Parse YAML content
|
||||
let workflow: Value =
|
||||
serde_yaml::from_str(&content).map_err(|e| format!("Invalid YAML: {}", e))?;
|
||||
|
||||
let mut result = ValidationResult::new();
|
||||
|
||||
// Check for required structure
|
||||
if !workflow.is_mapping() {
|
||||
result.add_issue("Workflow file is not a valid YAML mapping".to_string());
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Check if name exists
|
||||
if !workflow.get("name").is_some() {
|
||||
result.add_issue("Workflow is missing a name".to_string());
|
||||
}
|
||||
|
||||
// Check if jobs section exists
|
||||
match workflow.get("jobs") {
|
||||
Some(jobs) if jobs.is_mapping() => {
|
||||
validate_jobs(jobs, &mut result);
|
||||
}
|
||||
Some(_) => {
|
||||
result.add_issue("'jobs' section is not a mapping".to_string());
|
||||
}
|
||||
None => {
|
||||
result.add_issue("Workflow is missing 'jobs' section".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid triggers
|
||||
match workflow.get("on") {
|
||||
Some(on) => {
|
||||
validate_triggers(on, &mut result);
|
||||
}
|
||||
None => {
|
||||
result.add_issue("Workflow is missing 'on' section (triggers)".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if verbose && result.is_valid {
|
||||
println!(
|
||||
"{} Validated structure of workflow: {}",
|
||||
"✓".green(),
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
127
src/main.rs
Normal file
127
src/main.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
mod evaluator;
|
||||
mod models;
|
||||
mod utils;
|
||||
mod validators;
|
||||
|
||||
use clap::Parser;
|
||||
use colored::*;
|
||||
use evaluator::evaluate_workflow_file;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use utils::is_workflow_file;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "wrkflw", about = "GitHub Workflow evaluator")]
|
||||
struct Wrkflw {
|
||||
/// Path to the workflow file or directory containing workflow files
|
||||
path: PathBuf,
|
||||
|
||||
/// Run in verbose mode with detailed output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Wrkflw::parse();
|
||||
|
||||
let path = &opt.path;
|
||||
if !path.exists() {
|
||||
eprintln!("{}", "Error: Path does not exist".red());
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
evaluate_directory(path, opt.verbose);
|
||||
} else {
|
||||
match evaluate_workflow_file(path, opt.verbose) {
|
||||
Ok(result) => {
|
||||
if result.is_valid {
|
||||
println!(
|
||||
"{} {}",
|
||||
"✓".green(),
|
||||
format!("Workflow file is valid: {}", path.display()).green()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} {}",
|
||||
"✗".red(),
|
||||
format!("Workflow file has issues: {}", path.display()).red()
|
||||
);
|
||||
for (i, issue) in result.issues.iter().enumerate() {
|
||||
println!(" {}. {}", i + 1, issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
"✗".red(),
|
||||
format!("Error processing file {}", path.display()).red(),
|
||||
e
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_directory(dir_path: &PathBuf, verbose: bool) {
|
||||
use std::fs;
|
||||
|
||||
let mut valid_count = 0;
|
||||
let mut invalid_count = 0;
|
||||
|
||||
println!("Evaluating workflows in directory: {}", dir_path.display());
|
||||
|
||||
let entries = match fs::read_dir(dir_path) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Error reading directory: {}", e).red());
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_file() && is_workflow_file(&path) {
|
||||
match evaluate_workflow_file(&path, verbose) {
|
||||
Ok(result) => {
|
||||
if result.is_valid {
|
||||
println!(
|
||||
"{} {}",
|
||||
"✓".green(),
|
||||
format!("Valid: {}", path.display()).green()
|
||||
);
|
||||
valid_count += 1;
|
||||
} else {
|
||||
println!(
|
||||
"{} {}",
|
||||
"✗".red(),
|
||||
format!("Invalid: {}", path.display()).red()
|
||||
);
|
||||
for (i, issue) in result.issues.iter().enumerate() {
|
||||
println!(" {}. {}", i + 1, issue);
|
||||
}
|
||||
invalid_count += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
"✗".red(),
|
||||
format!("Error: {}", path.display()).red(),
|
||||
e
|
||||
);
|
||||
invalid_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nSummary: {} valid, {} invalid workflow files",
|
||||
valid_count, invalid_count
|
||||
);
|
||||
}
|
||||
18
src/models.rs
Normal file
18
src/models.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
pub struct ValidationResult {
|
||||
pub is_valid: bool,
|
||||
pub issues: Vec<String>,
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
pub fn new() -> Self {
|
||||
ValidationResult {
|
||||
is_valid: true,
|
||||
issues: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_issue(&mut self, issue: String) {
|
||||
self.is_valid = false;
|
||||
self.issues.push(issue);
|
||||
}
|
||||
}
|
||||
15
src/utils.rs
Normal file
15
src/utils.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub fn is_workflow_file(path: &Path) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "yml" || ext == "yaml" {
|
||||
// Check if the file is in a .github/workflows directory
|
||||
// Or accept any YAML file if specifically chosen
|
||||
if let Some(parent) = path.parent() {
|
||||
return parent.ends_with(".github/workflows")
|
||||
|| path.to_string_lossy().contains("workflow");
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
40
src/validators/actions.rs
Normal file
40
src/validators/actions.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::models::ValidationResult;
|
||||
|
||||
pub fn validate_action_reference(
|
||||
action_ref: &str,
|
||||
job_name: &str,
|
||||
step_idx: usize,
|
||||
result: &mut ValidationResult,
|
||||
) {
|
||||
// Check for valid action reference formats
|
||||
if !action_ref.contains('/') && !action_ref.contains('.') {
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Invalid action reference format '{}'",
|
||||
job_name,
|
||||
step_idx + 1,
|
||||
action_ref
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for version tag or commit SHA
|
||||
if action_ref.contains('@') {
|
||||
let parts: Vec<&str> = action_ref.split('@').collect();
|
||||
if parts.len() != 2 || parts[1].is_empty() {
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Action '{}' has invalid version/ref format",
|
||||
job_name,
|
||||
step_idx + 1,
|
||||
action_ref
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// Missing version tag is not recommended
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Action '{}' is missing version tag (@v2, @main, etc.)",
|
||||
job_name,
|
||||
step_idx + 1,
|
||||
action_ref
|
||||
));
|
||||
}
|
||||
}
|
||||
76
src/validators/jobs.rs
Normal file
76
src/validators/jobs.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::models::ValidationResult;
|
||||
use crate::validators::validate_steps;
|
||||
use serde_yaml::Value;
|
||||
|
||||
pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
|
||||
if let Value::Mapping(jobs_map) = jobs {
|
||||
if jobs_map.is_empty() {
|
||||
result.add_issue("'jobs' section is empty".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
for (job_name, job_config) in jobs_map {
|
||||
if let Some(job_name) = job_name.as_str() {
|
||||
if let Some(job_config) = job_config.as_mapping() {
|
||||
// Check for required 'runs-on'
|
||||
if !job_config.contains_key(&Value::String("runs-on".to_string())) {
|
||||
result.add_issue(format!("Job '{}' is missing 'runs-on' field", job_name));
|
||||
}
|
||||
|
||||
// Check for steps
|
||||
match job_config.get(&Value::String("steps".to_string())) {
|
||||
Some(Value::Sequence(steps)) => {
|
||||
if steps.is_empty() {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' has empty 'steps' section",
|
||||
job_name
|
||||
));
|
||||
} else {
|
||||
validate_steps(steps, job_name, result);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}': 'steps' section is not a sequence",
|
||||
job_name
|
||||
));
|
||||
}
|
||||
None => {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' is missing 'steps' section",
|
||||
job_name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for job dependencies
|
||||
if let Some(Value::Sequence(needs)) =
|
||||
job_config.get(&Value::String("needs".to_string()))
|
||||
{
|
||||
for need in needs {
|
||||
if let Some(need_str) = need.as_str() {
|
||||
if !jobs_map.contains_key(&Value::String(need_str.to_string())) {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' depends on non-existent job '{}'",
|
||||
job_name, need_str
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(Value::String(need)) =
|
||||
job_config.get(&Value::String("needs".to_string()))
|
||||
{
|
||||
if !jobs_map.contains_key(&Value::String(need.clone())) {
|
||||
result.add_issue(format!(
|
||||
"Job '{}' depends on non-existent job '{}'",
|
||||
job_name, need
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.add_issue(format!("Job '{}' configuration is not a mapping", job_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/validators/mod.rs
Normal file
9
src/validators/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod actions;
|
||||
mod jobs;
|
||||
mod steps;
|
||||
mod triggers;
|
||||
|
||||
pub use actions::validate_action_reference;
|
||||
pub use jobs::validate_jobs;
|
||||
pub use steps::validate_steps;
|
||||
pub use triggers::validate_triggers;
|
||||
42
src/validators/steps.rs
Normal file
42
src/validators/steps.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::models::ValidationResult;
|
||||
use crate::validators::validate_action_reference;
|
||||
use serde_yaml::Value;
|
||||
|
||||
pub fn validate_steps(steps: &Vec<Value>, job_name: &str, result: &mut ValidationResult) {
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
if let Some(step_map) = step.as_mapping() {
|
||||
if !step_map.contains_key(&Value::String("name".to_string()))
|
||||
&& !step_map.contains_key(&Value::String("uses".to_string()))
|
||||
&& !step_map.contains_key(&Value::String("run".to_string()))
|
||||
{
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Missing 'name', 'uses', or 'run' field",
|
||||
job_name,
|
||||
i + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Check for both 'uses' and 'run' in the same step
|
||||
if step_map.contains_key(&Value::String("uses".to_string()))
|
||||
&& step_map.contains_key(&Value::String("run".to_string()))
|
||||
{
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Contains both 'uses' and 'run' (should only use one)",
|
||||
job_name,
|
||||
i + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Validate action reference if 'uses' is present
|
||||
if let Some(Value::String(uses)) = step_map.get(&Value::String("uses".to_string())) {
|
||||
validate_action_reference(uses, job_name, i, result);
|
||||
}
|
||||
} else {
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Not a valid mapping",
|
||||
job_name,
|
||||
i + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/validators/triggers.rs
Normal file
83
src/validators/triggers.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::models::ValidationResult;
|
||||
use serde_yaml::Value;
|
||||
|
||||
pub fn validate_triggers(on: &Value, result: &mut ValidationResult) {
|
||||
let valid_events = vec![
|
||||
"push",
|
||||
"pull_request",
|
||||
"workflow_dispatch",
|
||||
"schedule",
|
||||
"repository_dispatch",
|
||||
"issue_comment",
|
||||
"issues",
|
||||
"label",
|
||||
"milestone",
|
||||
"page_build",
|
||||
"project",
|
||||
"project_card",
|
||||
"public",
|
||||
"release",
|
||||
"status",
|
||||
"watch",
|
||||
"workflow_run",
|
||||
];
|
||||
|
||||
match on {
|
||||
Value::String(event) => {
|
||||
if !valid_events.contains(&event.as_str()) {
|
||||
result.add_issue(format!("Unknown trigger event: '{}'", event));
|
||||
}
|
||||
}
|
||||
Value::Sequence(events) => {
|
||||
for event in events {
|
||||
if let Some(event_str) = event.as_str() {
|
||||
if !valid_events.contains(&event_str) {
|
||||
result.add_issue(format!("Unknown trigger event: '{}'", event_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Mapping(event_map) => {
|
||||
for (event, _) in event_map {
|
||||
if let Some(event_str) = event.as_str() {
|
||||
if !valid_events.contains(&event_str) {
|
||||
result.add_issue(format!("Unknown trigger event: '{}'", event_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check schedule syntax if present
|
||||
if let Some(Value::Sequence(schedules)) =
|
||||
event_map.get(&Value::String("schedule".to_string()))
|
||||
{
|
||||
for schedule in schedules {
|
||||
if let Some(schedule_map) = schedule.as_mapping() {
|
||||
if let Some(Value::String(cron)) =
|
||||
schedule_map.get(&Value::String("cron".to_string()))
|
||||
{
|
||||
validate_cron_syntax(cron, result);
|
||||
} else {
|
||||
result.add_issue("Schedule is missing 'cron' expression".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
result.add_issue("'on' section has invalid format".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_cron_syntax(cron: &str, result: &mut ValidationResult) {
|
||||
// Basic validation of cron syntax
|
||||
let parts: Vec<&str> = cron.split_whitespace().collect();
|
||||
if parts.len() != 5 {
|
||||
result.add_issue(format!(
|
||||
"Invalid cron syntax '{}': should have 5 components",
|
||||
cron
|
||||
));
|
||||
}
|
||||
|
||||
// More detailed validation could be added here
|
||||
}
|
||||
Reference in New Issue
Block a user