use clap::Parser;
use std::process;
use azdolint::azure::AzureDevOpsClient;
use azdolint::error::OutputFormatter;
use azdolint::parser::{
detect_template, extract_template_references, extract_variable_references,
extract_variable_references_from_content, parse_pipeline_file, resolve_template_path,
};
use azdolint::validator::{validate_variable_groups, validate_variables, VariableSource};
#[derive(Parser, Debug)]
#[command(name = "azdo-linter")]
#[command(about = "Validates Azure DevOps pipeline YAML variable references")]
struct Args {
#[arg(short, long)]
pipeline_file: String,
#[arg(short, long)]
organization: String,
#[arg(short = 'j', long)]
project: String,
#[arg(short, long, default_value_t = false)]
verbose: bool,
}
const EXIT_SUCCESS: i32 = 0;
const EXIT_VALIDATION_FAILURE: i32 = 1;
const EXIT_ERROR: i32 = 2;
fn main() {
let args = Args::parse();
if args.verbose {
println!("Pipeline file: {}", args.pipeline_file);
println!("Organization: {}", args.organization);
println!("Project: {}", args.project);
}
match run_validation(&args) {
Ok(has_failures) => {
if has_failures {
process::exit(EXIT_VALIDATION_FAILURE);
} else {
process::exit(EXIT_SUCCESS);
}
}
Err(e) => {
eprintln!("Error: {e}");
process::exit(EXIT_ERROR);
}
}
}
fn run_validation(args: &Args) -> Result<bool, anyhow::Error> {
println!("Azure DevOps Pipeline Validator");
println!("================================");
println!();
if args.verbose {
println!("{}", OutputFormatter::info(&format!("Parsing pipeline file: {}", args.pipeline_file)));
}
let template_info = detect_template(&args.pipeline_file)?;
if template_info.is_template {
println!(
"{}",
OutputFormatter::warning("This appears to be a template file")
);
println!();
println!(" Template files cannot be validated in isolation because they expect");
println!(" variables to be provided by the parent pipeline that includes them.");
println!();
if !template_info.parameter_names.is_empty() {
println!(" Template parameters defined:");
for param in &template_info.parameter_names {
println!(" - {param}");
}
println!();
}
println!(" To validate variables used in this template, run the linter against");
println!(" the parent pipeline that includes this template.");
println!();
println!("================================");
println!("RESULT: SKIPPED (template file)");
println!("================================");
return Ok(false); }
let pipeline = parse_pipeline_file(&args.pipeline_file)?;
let variable_groups = pipeline.get_variable_groups();
if args.verbose {
println!("{}", OutputFormatter::info(&format!("Found {} variable group(s) referenced", variable_groups.len())));
for group in &variable_groups {
println!(" - {group}");
}
}
let inline_variables = pipeline.get_inline_variable_names();
if args.verbose {
println!("{}", OutputFormatter::info(&format!("Found {} inline variable(s) defined", inline_variables.len())));
for var in &inline_variables {
println!(" - {var}");
}
}
let variable_references = extract_variable_references(&args.pipeline_file)?;
if args.verbose {
println!(
"{}",
OutputFormatter::info(&format!("Found {} variable reference(s) to validate", variable_references.len()))
);
for var in &variable_references {
println!(" - $({var})");
}
}
let client = AzureDevOpsClient::new(args.organization.clone(), args.project.clone());
if args.verbose {
println!("{}", OutputFormatter::info("Checking Azure CLI availability..."));
}
client.check_cli_available()?;
if args.verbose {
println!("{}", OutputFormatter::success("Azure CLI is available and configured"));
}
println!("{}", OutputFormatter::section("Variable Groups"));
let group_results = validate_variable_groups(variable_groups, &client)?;
let mut group_pass_count = 0;
let mut group_fail_count = 0;
for result in &group_results {
if result.exists {
group_pass_count += 1;
println!("{}", OutputFormatter::success(&format!("Variable group '{}' exists", result.group_name)));
} else {
group_fail_count += 1;
println!("{}", OutputFormatter::failure(&format!("Variable group '{}' not found", result.group_name)));
if let Some(ref error) = result.error {
if args.verbose {
println!(" Error: {error}");
}
}
println!(
" Suggestion: Create the variable group in Azure DevOps at:\n https://dev.azure.com/{}/{}/_library?itemType=VariableGroups",
args.organization, args.project
);
}
}
if group_results.is_empty() {
println!("{}", OutputFormatter::info("No variable groups referenced in pipeline"));
}
println!("{}", OutputFormatter::section("Variable References"));
let variable_results = validate_variables(variable_references, &group_results, &inline_variables, &client)?;
let mut var_pass_count = 0;
let mut var_fail_count = 0;
for result in &variable_results {
if result.exists {
var_pass_count += 1;
match &result.source {
VariableSource::Group(group_name) => {
println!(
"{}",
OutputFormatter::success(&format!("Variable '{}' found in group '{}'", result.variable_name, group_name))
);
}
VariableSource::Inline => {
println!(
"{}",
OutputFormatter::success(&format!("Variable '{}' defined inline in pipeline", result.variable_name))
);
}
VariableSource::NotFound => {
println!("{}", OutputFormatter::success(&format!("Variable '{}' found", result.variable_name)));
}
}
} else {
var_fail_count += 1;
println!(
"{}",
OutputFormatter::failure(&format!("Variable '{}' not found in any referenced group", result.variable_name))
);
if let Some(ref error) = result.error {
if args.verbose {
println!(" Error: {error}");
}
}
println!(" Suggestion: Add this variable to one of the referenced variable groups,");
println!(" or verify the variable name is spelled correctly.");
}
}
if variable_results.is_empty() {
println!("{}", OutputFormatter::info("No variable references found in pipeline"));
}
let template_refs = extract_template_references(&args.pipeline_file)?;
let mut template_pass_count = 0;
let mut template_fail_count = 0;
if !template_refs.is_empty() {
for template_ref in &template_refs {
let resolved_path = resolve_template_path(&args.pipeline_file, &template_ref.template_path);
let stage_info = template_ref
.stage_name
.as_ref()
.map(|s| format!(" (stage: {s})"))
.unwrap_or_default();
let groups_info = if template_ref.available_groups.is_empty() {
String::new()
} else {
format!(", groups: {}", template_ref.available_groups.join(", "))
};
println!(
"{}",
OutputFormatter::section(&format!(
"Template: {}{}{}",
template_ref.template_path, stage_info, groups_info
))
);
if !std::path::Path::new(&resolved_path).exists() {
println!(
"{}",
OutputFormatter::warning(&format!(
"Template file not found: {} (resolved to: {})",
template_ref.template_path, resolved_path
))
);
println!(" The template may be in a different repository or location.");
continue;
}
let template_content = std::fs::read_to_string(&resolved_path)?;
let template_var_refs = extract_variable_references_from_content(&template_content)?;
if template_var_refs.is_empty() {
println!(
"{}",
OutputFormatter::info("No variable references found in template")
);
continue;
}
if args.verbose {
println!(
"{}",
OutputFormatter::info(&format!(
"Found {} variable reference(s) in template",
template_var_refs.len()
))
);
}
let new_groups: Vec<String> = template_ref
.available_groups
.iter()
.filter(|g| !group_results.iter().any(|r| &r.group_name == *g))
.cloned()
.collect();
let template_group_results = if !new_groups.is_empty() {
validate_variable_groups(new_groups, &client)?
} else {
Vec::new()
};
let all_group_results: Vec<_> = group_results
.iter()
.chain(template_group_results.iter())
.filter(|r| template_ref.available_groups.contains(&r.group_name))
.cloned()
.collect();
let template_var_results = validate_variables(
template_var_refs,
&all_group_results,
&template_ref.available_inline_vars,
&client,
)?;
for result in &template_var_results {
if result.exists {
template_pass_count += 1;
match &result.source {
VariableSource::Group(group_name) => {
println!(
"{}",
OutputFormatter::success(&format!(
"Variable '{}' found in group '{}'",
result.variable_name, group_name
))
);
}
VariableSource::Inline => {
println!(
"{}",
OutputFormatter::success(&format!(
"Variable '{}' defined inline in parent pipeline",
result.variable_name
))
);
}
VariableSource::NotFound => {
println!(
"{}",
OutputFormatter::success(&format!("Variable '{}' found", result.variable_name))
);
}
}
} else {
template_fail_count += 1;
println!(
"{}",
OutputFormatter::failure(&format!(
"Variable '{}' not found in available groups",
result.variable_name
))
);
if !template_ref.available_groups.is_empty() {
println!(
" Available groups: {}",
template_ref.available_groups.join(", ")
);
}
println!(" Suggestion: Add this variable to one of the available variable groups.");
}
}
}
}
let total_passed = group_pass_count + var_pass_count + template_pass_count;
let total_failed = group_fail_count + var_fail_count + template_fail_count;
println!("{}", OutputFormatter::summary(total_passed, total_failed));
Ok(total_failed > 0)
}